awacke1 commited on
Commit
4c39985
ยท
verified ยท
1 Parent(s): 55906dc

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +571 -0
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import websockets
4
+ import uuid
5
+ from datetime import datetime
6
+ import os
7
+ import random
8
+ import hashlib
9
+ import base64
10
+ import edge_tts
11
+ import nest_asyncio
12
+ import re
13
+ import threading
14
+ import time
15
+ import json
16
+ 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 folium_static
21
+
22
+ # Patch asyncio for nesting
23
+ nest_asyncio.apply()
24
+
25
+ # Page Config
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
+ "Trailblazer Tim ๐ŸŒ„": {"voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00},
37
+ "Meme Queen Mia ๐Ÿ˜‚": {"voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff},
38
+ "Elk Whisperer Eve ๐ŸฆŒ": {"voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff},
39
+ "Tech Titan Tara ๐Ÿ’พ": {"voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00},
40
+ "Ski Guru Sam โ›ท๏ธ": {"voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500},
41
+ "Cosmic Camper Cal ๐ŸŒ ": {"voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080},
42
+ "Rasta Ranger Rick ๐Ÿƒ": {"voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff},
43
+ "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),
51
+ "Wyoming Spring Creek": (41.6666, -106.6666)
52
+ }
53
+
54
+ # Directories
55
+ for d in ["chat_logs", "audio_logs"]:
56
+ os.makedirs(d, exist_ok=True)
57
+
58
+ CHAT_DIR = "chat_logs"
59
+ AUDIO_DIR = "audio_logs"
60
+ STATE_FILE = "user_state.txt"
61
+ CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
62
+
63
+ # Helpers
64
+ def format_timestamp(username=""):
65
+ now = datetime.now().strftime("%Y%m%d_%H%M%S")
66
+ return f"{now}-by-{username}"
67
+
68
+ def clean_text_for_tts(text):
69
+ return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
70
+
71
+ def generate_filename(prompt, username, file_type="md"):
72
+ timestamp = format_timestamp(username)
73
+ hash_val = hashlib.md5(prompt.encode()).hexdigest()[:8]
74
+ return f"{timestamp}-{hash_val}.{file_type}"
75
+
76
+ def create_file(prompt, username, file_type="md"):
77
+ filename = generate_filename(prompt, username, file_type)
78
+ with open(filename, 'w', encoding='utf-8') as f:
79
+ f.write(prompt)
80
+ return filename
81
+
82
+ def get_download_link(file, file_type="mp3"):
83
+ with open(file, "rb") as f:
84
+ b64 = base64.b64encode(f.read()).decode()
85
+ mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"}
86
+ 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>'
87
+
88
+ def save_username(username):
89
+ with open(STATE_FILE, 'w') as f:
90
+ f.write(username)
91
+
92
+ def load_username():
93
+ if os.path.exists(STATE_FILE):
94
+ with open(STATE_FILE, 'r') as f:
95
+ return f.read().strip()
96
+ return None
97
+
98
+ # Audio Processing
99
+ async def async_edge_tts_generate(text, voice, username):
100
+ cache_key = f"{text[:100]}_{voice}"
101
+ if cache_key in st.session_state['audio_cache']:
102
+ return st.session_state['audio_cache'][cache_key]
103
+ text = clean_text_for_tts(text)
104
+ filename = f"{AUDIO_DIR}/{format_timestamp(username)}-{hashlib.md5(text.encode()).hexdigest()[:8]}.mp3"
105
+ communicate = edge_tts.Communicate(text, voice)
106
+ await communicate.save(filename)
107
+ if os.path.exists(filename) and os.path.getsize(filename) > 0:
108
+ st.session_state['audio_cache'][cache_key] = filename
109
+ return filename
110
+ return None
111
+
112
+ def play_and_download_audio(file_path):
113
+ if file_path and os.path.exists(file_path):
114
+ st.audio(file_path)
115
+ st.markdown(get_download_link(file_path), unsafe_allow_html=True)
116
+
117
+ # WebSocket Broadcast
118
+ async def broadcast_message(message, room_id):
119
+ if room_id in st.session_state.active_connections:
120
+ disconnected = []
121
+ for client_id, ws in st.session_state.active_connections[room_id].items():
122
+ try:
123
+ await ws.send(message)
124
+ except websockets.ConnectionClosed:
125
+ disconnected.append(client_id)
126
+ for client_id in disconnected:
127
+ if client_id in st.session_state.active_connections[room_id]:
128
+ del st.session_state.active_connections[room_id][client_id]
129
+
130
+ # Chat and Quest Log
131
+ async def save_chat_entry(username, message, voice, is_markdown=False):
132
+ if not message.strip() or message == st.session_state.get('last_transcript', ''):
133
+ return None, None
134
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
135
+ entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
136
+ md_file = create_file(entry, username, "md")
137
+ with open(CHAT_FILE, 'a') as f:
138
+ f.write(f"{entry}\n")
139
+ audio_file = await async_edge_tts_generate(message, voice, username)
140
+ await broadcast_message(f"{username}|{message}", "quest")
141
+ st.session_state.chat_history.append(entry)
142
+ st.session_state.last_transcript = message
143
+ st.session_state.score += 10
144
+ st.session_state.treasures += 1
145
+ st.session_state.last_chat_update = time.time()
146
+ return md_file, audio_file
147
+
148
+ async def load_chat():
149
+ if not os.path.exists(CHAT_FILE):
150
+ with open(CHAT_FILE, 'a') as f:
151
+ f.write(f"# {GAME_NAME} Log\n\nThe adventure begins at {START_LOCATION}! ๐Ÿ”๏ธ\n")
152
+ with open(CHAT_FILE, 'r') as f:
153
+ content = f.read().strip()
154
+ return content.split('\n')
155
+
156
+ # Session State Init
157
+ def init_session_state():
158
+ defaults = {
159
+ 'server_running': False, 'server_task': None, 'active_connections': {},
160
+ 'chat_history': [], 'audio_cache': {}, 'last_transcript': "",
161
+ 'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
162
+ 'speech_processed': False, 'players': {}, 'last_update': time.time(),
163
+ 'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
164
+ 'move_right': False, 'move_up': False, 'move_down': False,
165
+ 'prairie_players': {}, 'last_chat_update': 0
166
+ }
167
+ for k, v in defaults.items():
168
+ if k not in st.session_state:
169
+ st.session_state[k] = v
170
+ if st.session_state.username is None:
171
+ saved_username = load_username()
172
+ if saved_username and saved_username in CHARACTERS:
173
+ st.session_state.username = saved_username
174
+ else:
175
+ st.session_state.username = random.choice(list(CHARACTERS.keys()))
176
+ asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Begins the Rocky Mountain Quest!", CHARACTERS[st.session_state.username]["voice"]))
177
+ save_username(st.session_state.username)
178
+
179
+ init_session_state()
180
+
181
+ # WebSocket for Multiplayer
182
+ async def websocket_handler(websocket, path):
183
+ client_id = str(uuid.uuid4())
184
+ room_id = "quest"
185
+ if room_id not in st.session_state.active_connections:
186
+ st.session_state.active_connections[room_id] = {}
187
+ st.session_state.active_connections[room_id][client_id] = websocket
188
+ username = st.session_state.username
189
+
190
+ if "prairie" in path:
191
+ st.session_state.prairie_players[client_id] = {
192
+ "username": username,
193
+ "animal": "prairie_dog",
194
+ "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
195
+ "color": CHARACTERS[username]["color"]
196
+ }
197
+ await broadcast_message(f"System|{username} joins the prairie!", room_id)
198
+ else:
199
+ st.session_state.players[client_id] = {
200
+ "username": username,
201
+ "x": random.uniform(-20, 20),
202
+ "z": random.uniform(-50, 50),
203
+ "color": CHARACTERS[username]["color"],
204
+ "score": 0,
205
+ "treasures": 0
206
+ }
207
+ await broadcast_message(f"System|{username} joins the quest!", room_id)
208
+
209
+ try:
210
+ async for message in websocket:
211
+ if '|' in message:
212
+ sender, content = message.split('|', 1)
213
+ voice = CHARACTERS.get(sender, {"voice": "en-US-AriaNeural"})["voice"]
214
+ if content.startswith("MOVE:"):
215
+ _, x, z = content.split(":")
216
+ st.session_state.players[client_id]["x"] = float(x)
217
+ st.session_state.players[client_id]["z"] = float(z)
218
+ elif content.startswith("PRAIRIE:"):
219
+ action, value = content.split(":", 1)
220
+ if action == "ANIMAL":
221
+ st.session_state.prairie_players[client_id]["animal"] = value
222
+ elif action == "MOVE":
223
+ target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
224
+ st.session_state.prairie_players[client_id]["location"] = target
225
+ action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
226
+ await save_chat_entry(sender, action_msg, voice)
227
+ elif content.startswith("SCORE:"):
228
+ st.session_state.players[client_id]["score"] = int(content.split(":")[1])
229
+ elif content.startswith("TREASURE:"):
230
+ st.session_state.players[client_id]["treasures"] = int(content.split(":")[1])
231
+ else:
232
+ await save_chat_entry(sender, content, voice)
233
+ await perform_arxiv_search(content, sender)
234
+ except websockets.ConnectionClosed:
235
+ await broadcast_message(f"System|{username} leaves the quest!", room_id)
236
+ if client_id in st.session_state.players:
237
+ del st.session_state.players[client_id]
238
+ if client_id in st.session_state.prairie_players:
239
+ del st.session_state.prairie_players[client_id]
240
+ finally:
241
+ if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
242
+ del st.session_state.active_connections[room_id][client_id]
243
+
244
+ async def periodic_update():
245
+ while True:
246
+ if st.session_state.active_connections.get("quest"):
247
+ player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!"
248
+ message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
249
+ player_data = json.dumps(list(st.session_state.players.values()))
250
+ await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
251
+
252
+ prairie_list = ", ".join([f"{p['username']} ({p['animal']})" for p in st.session_state.prairie_players.values()]) or "No animals yet!"
253
+ prairie_message = f"๐ŸŒพ Prairie Update: {prairie_list}"
254
+ prairie_data = json.dumps(list(st.session_state.prairie_players.values()))
255
+ await broadcast_message(f"PRAIRIE_UPDATE:{prairie_data}", "quest")
256
+
257
+ # Multicast chat update every second
258
+ chat_content = await load_chat()
259
+ await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest")
260
+ await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
261
+ await asyncio.sleep(st.session_state.update_interval)
262
+
263
+ async def run_websocket_server():
264
+ if not st.session_state.get('server_running', False):
265
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
266
+ st.session_state['server_running'] = True
267
+ asyncio.create_task(periodic_update())
268
+ await server.wait_closed()
269
+
270
+ def start_websocket_server():
271
+ loop = asyncio.new_event_loop()
272
+ asyncio.set_event_loop(loop)
273
+ loop.run_until_complete(run_websocket_server())
274
+
275
+ # ArXiv Integration
276
+ async def perform_arxiv_search(query, username):
277
+ gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
278
+ refs = gradio_client.predict(
279
+ query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
280
+ )[0]
281
+ result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
282
+ voice = CHARACTERS[username]["voice"]
283
+ md_file, audio_file = await save_chat_entry(username, result, voice, True)
284
+ return md_file, audio_file
285
+
286
+ # Enhanced 3D Game HTML
287
+ rocky_map_html = f"""
288
+ <!DOCTYPE html>
289
+ <html lang="en">
290
+ <head>
291
+ <meta charset="UTF-8">
292
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
293
+ <title>Rocky Mountain Quest 3D</title>
294
+ <style>
295
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
296
+ #gameContainer {{ width: 800px; height: 600px; position: relative; }}
297
+ canvas {{ width: 100%; height: 100%; display: block; }}
298
+ #ui {{
299
+ position: absolute; top: 10px; left: 10px; color: white;
300
+ background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
301
+ }}
302
+ #chatBox {{
303
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
304
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
305
+ border-radius: 5px; overflow-y: auto;
306
+ }}
307
+ #controls {{
308
+ position: absolute; bottom: 10px; left: 10px; color: white;
309
+ background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 5px;
310
+ }}
311
+ </style>
312
+ </head>
313
+ <body>
314
+ <div id="gameContainer">
315
+ <div id="ui">
316
+ <div id="players">Players: 1</div>
317
+ <div id="score">Score: 0</div>
318
+ <div id="treasures">Treasures: 0</div>
319
+ </div>
320
+ <div id="chatBox"></div>
321
+ <div id="controls">WASD/Arrows to move, Space to collect treasure</div>
322
+ </div>
323
+
324
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
325
+ <script>
326
+ const playerName = "{st.session_state.username}";
327
+ let ws = new WebSocket('ws://localhost:8765');
328
+ const scene = new THREE.Scene();
329
+ const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
330
+ camera.position.set(0, 50, 50);
331
+ camera.lookAt(0, 0, 0);
332
+
333
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
334
+ renderer.setSize(800, 600);
335
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
336
+
337
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
338
+ scene.add(ambientLight);
339
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
340
+ sunLight.position.set(50, 50, 50);
341
+ sunLight.castShadow = true;
342
+ scene.add(sunLight);
343
+
344
+ const groundGeometry = new THREE.PlaneGeometry(100, 100);
345
+ const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
346
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
347
+ ground.rotation.x = -Math.PI / 2;
348
+ ground.receiveShadow = true;
349
+ scene.add(ground);
350
+
351
+ let players = {{}};
352
+ const playerMeshes = {{}};
353
+ let treasures = [];
354
+ let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
355
+ let score = 0, treasureCount = 0;
356
+
357
+ // Player initialization
358
+ const playerGeometry = new THREE.BoxGeometry(2, 2, 2);
359
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {CHARACTERS[st.session_state.username]["color"]} }});
360
+ const playerMesh = new THREE.Mesh(playerGeometry, playerMaterial);
361
+ playerMesh.position.set(xPos, 1, zPos);
362
+ playerMesh.castShadow = true;
363
+ scene.add(playerMesh);
364
+ players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
365
+
366
+ // Treasure spawning
367
+ function spawnTreasure() {{
368
+ if (treasures.length < 5) {{
369
+ const treasure = new THREE.Mesh(
370
+ new THREE.SphereGeometry(1, 8, 8),
371
+ new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
372
+ );
373
+ treasure.position.set(
374
+ Math.random() * 80 - 40,
375
+ 1,
376
+ Math.random() * 80 - 40
377
+ );
378
+ treasure.castShadow = true;
379
+ treasures.push(treasure);
380
+ scene.add(treasure);
381
+ }}
382
+ }}
383
+
384
+ // Controls
385
+ document.addEventListener('keydown', (event) => {{
386
+ switch (event.code) {{
387
+ case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
388
+ case 'ArrowRight': case 'KeyD': moveRight = true; break;
389
+ case 'ArrowUp': case 'KeyW': moveUp = true; break;
390
+ case 'ArrowDown': case 'KeyS': moveDown = true; break;
391
+ case 'Space': collect = true; break;
392
+ }}
393
+ }});
394
+ document.addEventListener('keyup', (event) => {{
395
+ switch (event.code) {{
396
+ case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
397
+ case 'ArrowRight': case 'KeyD': moveRight = false; break;
398
+ case 'ArrowUp': case 'KeyW': moveUp = false; break;
399
+ case 'ArrowDown': case 'KeyS': moveDown = false; break;
400
+ case 'Space': collect = false; break;
401
+ }}
402
+ }});
403
+
404
+ function updatePlayer(delta) {{
405
+ const speed = 20;
406
+ if (moveLeft && xPos > -40) xPos -= speed * delta;
407
+ if (moveRight && xPos < 40) xPos += speed * delta;
408
+ if (moveUp && zPos > -40) zPos -= speed * delta;
409
+ if (moveDown && zPos < 40) zPos += speed * delta;
410
+ playerMesh.position.set(xPos, 1, zPos);
411
+ ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
412
+
413
+ if (collect) {{
414
+ for (let i = treasures.length - 1; i >= 0; i--) {{
415
+ if (playerMesh.position.distanceTo(treasures[i].position) < 2) {{
416
+ scene.remove(treasures[i]);
417
+ treasures.splice(i, 1);
418
+ score += 50;
419
+ treasureCount += 1;
420
+ ws.send(`${{playerName}}|SCORE:${{score}}`);
421
+ ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
422
+ spawnTreasure();
423
+ }}
424
+ }}
425
+ }}
426
+
427
+ camera.position.set(xPos, 50, zPos + 50);
428
+ camera.lookAt(xPos, 0, zPos);
429
+ }}
430
+
431
+ function updateTreasures(delta) {{
432
+ treasures.forEach(treasure => {{
433
+ treasure.rotation.y += delta;
434
+ }});
435
+ if (Math.random() < 0.01) spawnTreasure();
436
+ }}
437
+
438
+ function updatePlayers(playerData) {{
439
+ playerData.forEach(player => {{
440
+ if (!playerMeshes[player.username]) {{
441
+ const mesh = new THREE.Mesh(playerGeometry, new THREE.MeshPhongMaterial({{ color: player.color }}));
442
+ mesh.castShadow = true;
443
+ scene.add(mesh);
444
+ playerMeshes[player.username] = mesh;
445
+ }}
446
+ const mesh = playerMeshes[player.username];
447
+ mesh.position.set(player.x, 1, player.z);
448
+ players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
449
+ }});
450
+ document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
451
+ document.getElementById('score').textContent = `Score: ${{score}}`;
452
+ document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
453
+ }}
454
+
455
+ ws.onmessage = function(event) {{
456
+ const data = event.data;
457
+ if (data.startsWith('MAP_UPDATE:')) {{
458
+ const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
459
+ updatePlayers(playerData);
460
+ }} else if (data.startsWith('CHAT_UPDATE:')) {{
461
+ const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
462
+ const chatBox = document.getElementById('chatBox');
463
+ chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
464
+ chatBox.scrollTop = chatBox.scrollHeight;
465
+ }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
466
+ const [sender, message] = data.split('|');
467
+ const chatBox = document.getElementById('chatBox');
468
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
469
+ chatBox.scrollTop = chatBox.scrollHeight;
470
+ }}
471
+ }};
472
+
473
+ let lastTime = performance.now();
474
+ function animate() {{
475
+ requestAnimationFrame(animate);
476
+ const currentTime = performance.now();
477
+ const delta = (currentTime - lastTime) / 1000;
478
+ lastTime = currentTime;
479
+
480
+ updatePlayer(delta);
481
+ updateTreasures(delta);
482
+ renderer.render(scene, camera);
483
+ }}
484
+
485
+ // Initial spawn
486
+ for (let i = 0; i < 5; i++) spawnTreasure();
487
+ animate();
488
+ </script>
489
+ </body>
490
+ </html>
491
+ """
492
+
493
+ # Main Game Loop
494
+ def main():
495
+ st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
496
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: {st.session_state.score} ๐Ÿ†")
497
+ st.sidebar.write(f"๐Ÿ“œ {CHARACTERS[st.session_state.username]['desc']}")
498
+ st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
499
+ st.sidebar.write(f"๐Ÿ… Score: {st.session_state.score}")
500
+ st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
501
+ st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
502
+
503
+ new_username = st.sidebar.selectbox("๐Ÿง™โ€โ™‚๏ธ Choose Your Hero", list(CHARACTERS.keys()), index=list(CHARACTERS.keys()).index(st.session_state.username))
504
+ if new_username != st.session_state.username:
505
+ asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ”„ Transforms into {new_username}!", CHARACTERS[st.session_state.username]["voice"]))
506
+ st.session_state.username = new_username
507
+ save_username(st.session_state.username)
508
+ st.rerun()
509
+
510
+ left_col, right_col = st.columns([2, 1])
511
+
512
+ with left_col:
513
+ components.html(rocky_map_html, width=800, height=600)
514
+ chat_content = asyncio.run(load_chat())
515
+ st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
516
+ message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ")
517
+ if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"):
518
+ if message:
519
+ voice = CHARACTERS[st.session_state.username]["voice"]
520
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
521
+ if audio_file:
522
+ play_and_download_audio(audio_file)
523
+ st.success(f"๐ŸŒ„ +10 points! New Score: {st.session_state.score}")
524
+ mycomponent = components.declare_component("speech_component", path="./speech_component")
525
+ val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
526
+ if val and val != st.session_state.last_transcript:
527
+ val_stripped = val.strip().replace('\n', ' ')
528
+ if val_stripped:
529
+ voice = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"]
530
+ st.session_state['speech_processed'] = True
531
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
532
+ if audio_file:
533
+ play_and_download_audio(audio_file)
534
+ st.rerun()
535
+
536
+ with right_col:
537
+ st.subheader("๐ŸŒพ Prairie Map")
538
+ prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
539
+ for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
540
+ folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
541
+ for client_id, player in st.session_state.prairie_players.items():
542
+ folium.CircleMarker(
543
+ location=player['location'],
544
+ radius=5,
545
+ color=f"#{player['color']:06x}",
546
+ fill=True,
547
+ fill_opacity=0.7,
548
+ popup=f"{player['username']} ({player['animal']})"
549
+ ).add_to(prairie_map)
550
+ folium_static(prairie_map, width=600, height=400)
551
+
552
+ animal = st.selectbox("Choose Animal", ["prairie_dog", "deer", "sheep", "groundhog"])
553
+ location = st.selectbox("Move to", list(PRAIRIE_LOCATIONS.keys()))
554
+ if st.button("Move"):
555
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
556
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
557
+ st.rerun()
558
+
559
+ elapsed = time.time() - st.session_state.last_update
560
+ remaining = max(0, st.session_state.update_interval - elapsed)
561
+ st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
562
+ if remaining <= 0:
563
+ st.session_state.last_update = time.time()
564
+ st.rerun()
565
+
566
+ if not st.session_state.get('server_running', False):
567
+ st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
568
+ st.session_state.server_task.start()
569
+
570
+ if __name__ == "__main__":
571
+ main()