awacke1 commited on
Commit
cb19e9a
ยท
verified ยท
1 Parent(s): 6c4d06c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +797 -65
app.py CHANGED
@@ -23,10 +23,10 @@ import pytz
23
  from collections import defaultdict
24
  import pandas as pd
25
 
26
- # ๐Ÿ› ๏ธ (1) Allow nested asyncio loops for compatibility
27
  nest_asyncio.apply()
28
 
29
- # ๐ŸŽจ (2) Page Configuration for Streamlit UI
30
  st.set_page_config(
31
  layout="wide",
32
  page_title="Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ",
@@ -39,81 +39,813 @@ st.set_page_config(
39
  }
40
  )
41
 
42
- # ๐ŸŒŸ (3) Prominent Player Join Notifications
43
- async def broadcast_player_join(username):
44
- message = json.dumps({"type": "player_joined", "username": username})
45
- await broadcast_message(message, "quest")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- # ๐Ÿ”„ (4) Real-Time Game State Synchronization
48
- async def broadcast_game_state():
49
- while True:
50
- game_state = load_game_state(time.time())
51
- await broadcast_message(json.dumps({"type": "sync", "state": game_state}), "quest")
52
- await asyncio.sleep(1)
53
-
54
- # ๐ŸŒŒ (5) Fractal and Emergent Data Generation for 3D Visualization
55
- FRAC_SEEDS = [random.uniform(-40, 40) for _ in range(100)]
56
-
57
- # ๐ŸŽฒ (6) Three.js Shape Primitives and Fractal Data Structures
58
- shape_primitives = ['sphere', 'cube', 'cylinder', 'cone', 'torus', 'dodecahedron', 'octahedron', 'tetrahedron', 'icosahedron']
59
- fractals = [{
60
- "type": random.choice(shape_primitives),
61
- "position": [random.uniform(-40,40), random.uniform(0,20), random.uniform(-40,40)],
62
- "color": random.randint(0, 0xFFFFFF)
63
- } for _ in range(100)]
64
-
65
- # ๐Ÿ–ฅ๏ธ (7) Initialize WebSocket Server for Multiplayer Connectivity
66
- if not st.session_state.get('server_running', False):
67
- threading.Thread(target=start_websocket_server, daemon=True).start()
68
- st.session_state['server_running'] = True
69
-
70
- # ๐Ÿ’ฐ (8) Increment Scores and Automatically Save at Milestones
71
- async def increment_and_save_scores():
72
- while True:
73
- game_state = load_game_state(time.time())
74
- for player in game_state["players"].values():
75
- player["score"] += 1
76
- if player["score"] % 100 == 0:
77
- filename = f"cache_{player['username']}_score_{player['score']}.json"
78
- with open(filename, 'w') as f:
79
- json.dump(player, f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  update_game_state(game_state)
81
- await asyncio.sleep(1)
82
 
83
- # ๐Ÿš€ (9) Start Background Async Tasks for Game Operations
84
- asyncio.run(increment_and_save_scores())
85
- asyncio.run(broadcast_game_state())
86
 
87
- # ๐ŸŒ (10) Main Streamlit UI Function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- def main():
90
- # ๐Ÿ”๏ธ Game Title Display
91
- st.title("Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ")
 
 
 
 
 
 
 
 
 
92
 
93
- # ๐Ÿ“ UI Layout with Columns
94
- col1, col2 = st.columns([3, 1])
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- # ๐ŸŽฎ Left Column - Three.js Game Canvas
97
- with col1:
98
- components.html(rocky_map_html, width=800, height=600)
 
 
 
99
 
100
- # ๐Ÿ“‹ Right Column - Player Information and Maps
101
- with col2:
102
- st.subheader("Active Players ๐Ÿง™โ€โ™‚๏ธ")
103
- current_state = load_game_state(time.time())
104
- for player in current_state["players"].values():
105
- st.write(f"๐Ÿ‘ค {player['username']} - ๐ŸŒŸ {player['score']} points")
106
 
107
- st.subheader("๐ŸŒพ Prairie Map")
108
- prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8)
109
- folium_static(prairie_map)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- # ๐ŸŽฏ Sidebar Game Controls
112
- st.sidebar.markdown("### ๐ŸŽฏ Game Controls")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  if st.sidebar.button("Reset World ๐ŸŒ"):
114
  reset_game_state()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- # โœ… Run the application
118
  if __name__ == "__main__":
119
- main()
 
23
  from collections import defaultdict
24
  import pandas as pd
25
 
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
  }
40
  )
41
 
42
+ # Game Config
43
+ GAME_NAME = "Rocky Mountain Quest 3D ๐Ÿ”๏ธ๐ŸŽฎ"
44
+ START_LOCATION = "Trailhead Camp โ›บ"
45
+ EDGE_TTS_VOICES = [
46
+ {"name": "Aria ๐ŸŒธ", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4, "shape": "sphere"},
47
+ {"name": "Guy ๐ŸŒŸ", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00, "shape": "cube"},
48
+ {"name": "Jenny ๐ŸŽถ", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00, "shape": "cylinder"},
49
+ {"name": "Sonia ๐ŸŒบ", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF, "shape": "cone"},
50
+ {"name": "Ryan ๐Ÿ› ๏ธ", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF, "shape": "torus"},
51
+ {"name": "Natasha ๐ŸŒŒ", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080, "shape": "dodecahedron"},
52
+ {"name": "William ๐ŸŽป", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500, "shape": "octahedron"},
53
+ {"name": "Clara ๐ŸŒท", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500, "shape": "tetrahedron"},
54
+ {"name": "Liam ๐ŸŒŸ", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700, "shape": "icosahedron"}
55
+ ]
56
+ FILE_EMOJIS = {"md": "๐Ÿ“œ", "mp3": "๐ŸŽต"}
57
+ WORLD_OBJECTS = [
58
+ {"type": "๐ŸŒฟ Plants", "emoji": "๐ŸŒฑ๐ŸŒฟ๐ŸŒพ", "color": 0x228B22, "shape": "cylinder", "count": 10},
59
+ {"type": "๐Ÿฆ Animal Flocks", "emoji": "๐Ÿ•Š๏ธ๐Ÿฆ๐Ÿค", "color": 0x87CEEB, "shape": "sphere", "count": 5},
60
+ {"type": "๐Ÿ”๏ธ Mountains", "emoji": "๐Ÿ”๏ธโ›ฐ๏ธ", "color": 0x808080, "shape": "cone", "count": 3},
61
+ {"type": "๐ŸŒณ Trees", "emoji": "๐ŸŒฒ๐ŸŒณ๐ŸŒด", "color": 0x006400, "shape": "cylinder", "count": 8},
62
+ {"type": "๐Ÿพ Animals", "emoji": "๐Ÿป๐Ÿบ๐ŸฆŒ", "color": 0x8B4513, "shape": "cube", "count": 6}
63
+ ]
64
+ PRAIRIE_LOCATIONS = {
65
+ "Deadwood, SD": (44.3769, -103.7298),
66
+ "Wind Cave National Park": (43.6047, -103.4798),
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
+
74
+ CHAT_DIR = "chat_logs"
75
+ AUDIO_DIR = "audio_logs"
76
+ STATE_FILE = "user_state.txt"
77
+ CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md")
78
+ GAME_STATE_FILE = "game_state.json"
79
+
80
+ # Cached Game State as "Resource"
81
+ @st.cache_resource
82
+ def load_game_state(_timestamp):
83
+ """Load or initialize the game state, treated as a cached resource."""
84
+ if os.path.exists(GAME_STATE_FILE):
85
+ with open(GAME_STATE_FILE, 'r') as f:
86
+ state = json.load(f)
87
+ else:
88
+ state = {
89
+ "players": {},
90
+ "treasures": [
91
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
92
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
93
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
94
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
95
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
96
+ ],
97
+ "world_objects": [],
98
+ "history": []
99
+ }
100
+ with open(GAME_STATE_FILE, 'w') as f:
101
+ json.dump(state, f)
102
+ return state
103
+
104
+ def update_game_state(state):
105
+ """Update the game state and persist to file."""
106
+ state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time()
107
+ with open(GAME_STATE_FILE, 'w') as f:
108
+ json.dump(state, f)
109
+ load_game_state.clear()
110
+ return load_game_state(time.time())
111
+
112
+ def reset_game_state():
113
+ """Reset the game state to initial conditions."""
114
+ state = {
115
+ "players": {},
116
+ "treasures": [
117
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
118
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
119
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
120
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)},
121
+ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}
122
+ ],
123
+ "world_objects": [],
124
+ "history": []
125
+ }
126
+ return update_game_state(state)
127
+
128
+ # Agent Class for Players
129
+ class PlayerAgent:
130
+ def __init__(self, username, char_data):
131
+ self.username = username
132
+ self.x = random.uniform(-20, 20)
133
+ self.z = random.uniform(-40, 40)
134
+ self.color = char_data["color"]
135
+ self.shape = char_data["shape"]
136
+ self.score = 0
137
+ self.treasures = 0
138
+ self.last_active = time.time()
139
+ self.voice = char_data["voice"]
140
+
141
+ def to_dict(self):
142
+ return {
143
+ "username": self.username,
144
+ "x": self.x,
145
+ "z": self.z,
146
+ "color": self.color,
147
+ "shape": self.shape,
148
+ "score": self.score,
149
+ "treasures": self.treasures,
150
+ "last_active": self.last_active
151
+ }
152
+
153
+ def update_from_message(self, message):
154
+ if '|' in message:
155
+ _, content = message.split('|', 1)
156
+ if content.startswith("MOVE:"):
157
+ _, x, z = content.split(":")
158
+ self.x, self.z = float(x), float(z)
159
+ self.last_active = time.time()
160
+ elif content.startswith("SCORE:"):
161
+ self.score = int(content.split(":")[1])
162
+ self.last_active = time.time()
163
+ elif content.startswith("TREASURE:"):
164
+ self.treasures = int(content.split(":")[1])
165
+ self.last_active = time.time()
166
+
167
+ # Helpers
168
+ def format_timestamp(username=""):
169
+ now = datetime.now(pytz.timezone('US/Central')).strftime("%Y%m%d_%H%M%S")
170
+ return f"{now}-by-{username}"
171
+
172
+ def clean_text_for_tts(text):
173
+ return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text"
174
+
175
+ def generate_filename(prompt, username, file_type="md"):
176
+ timestamp = format_timestamp(username)
177
+ cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_")
178
+ return f"{cleaned_prompt}_{timestamp}.{file_type}"
179
+
180
+ def create_file(prompt, username, file_type="md"):
181
+ filename = generate_filename(prompt, username, file_type)
182
+ with open(filename, 'w', encoding='utf-8') as f:
183
+ f.write(prompt)
184
+ return filename
185
+
186
+ def get_download_link(file, file_type="mp3"):
187
+ with open(file, "rb") as f:
188
+ b64 = base64.b64encode(f.read()).decode()
189
+ mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"}
190
+ 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>'
191
+
192
+ def save_username(username):
193
+ with open(STATE_FILE, 'w') as f:
194
+ f.write(username)
195
+
196
+ def load_username():
197
+ if os.path.exists(STATE_FILE):
198
+ with open(STATE_FILE, 'r') as f:
199
+ return f.read().strip()
200
+ return None
201
+
202
+ # Audio Processing
203
+ async def async_edge_tts_generate(text, voice, username):
204
+ cache_key = f"{text[:100]}_{voice}"
205
+ if cache_key in st.session_state['audio_cache']:
206
+ return st.session_state['audio_cache'][cache_key]
207
+ text = clean_text_for_tts(text)
208
+ filename = generate_filename(text, username, "mp3")
209
+ communicate = edge_tts.Communicate(text, voice)
210
+ await communicate.save(filename)
211
+ if os.path.exists(filename) and os.path.getsize(filename) > 0:
212
+ st.session_state['audio_cache'][cache_key] = filename
213
+ return filename
214
+ return None
215
+
216
+ def play_and_download_audio(file_path):
217
+ if file_path and os.path.exists(file_path):
218
+ st.audio(file_path, start_time=0)
219
+ st.markdown(get_download_link(file_path), unsafe_allow_html=True)
220
+
221
+ # WebSocket Broadcast
222
+ async def broadcast_message(message, room_id):
223
+ if room_id in st.session_state.active_connections:
224
+ disconnected = []
225
+ for client_id, ws in st.session_state.active_connections[room_id].items():
226
+ try:
227
+ await ws.send(message)
228
+ except websockets.ConnectionClosed:
229
+ disconnected.append(client_id)
230
+ for client_id in disconnected:
231
+ if client_id in st.session_state.active_connections[room_id]:
232
+ del st.session_state.active_connections[room_id][client_id]
233
+
234
+ # Chat and Quest Log
235
+ async def save_chat_entry(username, message, voice, is_markdown=False):
236
+ if not message.strip() or message == st.session_state.get('last_transcript', ''):
237
+ return None, None
238
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
239
+ entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```"
240
+ md_file = create_file(entry, username, "md")
241
+ with open(CHAT_FILE, 'a') as f:
242
+ f.write(f"{entry}\n")
243
+ audio_file = await async_edge_tts_generate(message, voice, username)
244
+ await broadcast_message(f"{username}|{message}", "quest")
245
+ st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file})
246
+ st.session_state.last_transcript = message
247
+ game_state = load_game_state(st.session_state.game_state_timestamp)
248
+ if username in game_state["players"]:
249
+ game_state["players"][username]["score"] += 10
250
+ game_state["players"][username]["treasures"] += 1
251
+ game_state["players"][username]["last_active"] = time.time()
252
+ game_state["history"].append(entry)
253
+ if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]:
254
+ obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower())
255
+ for _ in range(obj_type["count"]):
256
+ game_state["world_objects"].append({
257
+ "type": obj_type["type"],
258
+ "emoji": random.choice(obj_type["emoji"].split()),
259
+ "x": random.uniform(-40, 40),
260
+ "z": random.uniform(-40, 40),
261
+ "color": obj_type["color"],
262
+ "shape": obj_type["shape"]
263
+ })
264
+ update_game_state(game_state)
265
+ return md_file, audio_file
266
+
267
+ async def load_chat():
268
+ if not os.path.exists(CHAT_FILE):
269
+ with open(CHAT_FILE, 'a') as f:
270
+ f.write(f"# {GAME_NAME} Log\n\nThe adventure begins at {START_LOCATION}! ๐Ÿ”๏ธ\n")
271
+ with open(CHAT_FILE, 'r') as f:
272
+ content = f.read().strip()
273
+ return content.split('\n')
274
+
275
+ # Session State Init
276
+ def init_session_state():
277
+ defaults = {
278
+ 'server_running': False, 'server_task': None, 'active_connections': {},
279
+ 'chat_history': [], 'audio_cache': {}, 'last_transcript': "",
280
+ 'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION,
281
+ 'speech_processed': False, 'players': {}, 'last_update': time.time(),
282
+ 'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False,
283
+ 'move_right': False, 'move_up': False, 'move_down': False,
284
+ 'prairie_players': {}, 'last_chat_update': 0, 'game_state_timestamp': time.time(),
285
+ 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time(),
286
+ 'marquee_settings': {"background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px", "animationDuration": "20s", "width": "100%", "lineHeight": "35px"},
287
+ 'operation_timings': {}, 'performance_metrics': defaultdict(list), 'download_link_cache': {}
288
+ }
289
+ for k, v in defaults.items():
290
+ if k not in st.session_state:
291
+ st.session_state[k] = v
292
+ if st.session_state.username is None:
293
+ char = random.choice(EDGE_TTS_VOICES)
294
+ st.session_state.username = char["name"]
295
+ st.session_state.tts_voice = char["voice"]
296
+ asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ—บ๏ธ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"]))
297
+ game_state = load_game_state(st.session_state.game_state_timestamp)
298
+ if st.session_state.username not in game_state["players"]:
299
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
300
+ agent = PlayerAgent(st.session_state.username, char)
301
+ game_state["players"][st.session_state.username] = agent.to_dict()
302
  update_game_state(game_state)
 
303
 
304
+ init_session_state()
 
 
305
 
306
+ # WebSocket for Multiplayer
307
+ async def websocket_handler(websocket, path):
308
+ client_id = str(uuid.uuid4())
309
+ room_id = "quest"
310
+ if room_id not in st.session_state.active_connections:
311
+ st.session_state.active_connections[room_id] = {}
312
+ st.session_state.active_connections[room_id][client_id] = websocket
313
+ username = st.session_state.username
314
+
315
+ game_state = load_game_state(st.session_state.game_state_timestamp)
316
+ if "prairie" in path:
317
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
318
+ st.session_state.prairie_players[client_id] = {
319
+ "username": username,
320
+ "animal": "prairie_dog",
321
+ "location": PRAIRIE_LOCATIONS["Deadwood, SD"],
322
+ "color": char["color"]
323
+ }
324
+ await broadcast_message(f"System|{username} joins the prairie!", room_id)
325
+ else:
326
+ if username not in game_state["players"]:
327
+ char = next(c for c in EDGE_TTS_VOICES if c["name"] == username)
328
+ agent = PlayerAgent(username, char)
329
+ game_state["players"][username] = agent.to_dict()
330
+ update_game_state(game_state)
331
+ st.session_state.players[client_id] = game_state["players"][username]
332
+ await broadcast_message(f"System|{username} joins the quest!", room_id)
333
+
334
+ try:
335
+ async for message in websocket:
336
+ if '|' in message:
337
+ sender, content = message.split('|', 1)
338
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender)
339
+ game_state = load_game_state(st.session_state.game_state_timestamp)
340
+ agent = PlayerAgent(sender, next(c for c in EDGE_TTS_VOICES if c["name"] == sender))
341
+ agent.update_from_message(f"{sender}|{content}")
342
+ if sender in game_state["players"]:
343
+ game_state["players"][sender] = agent.to_dict()
344
+ if content.startswith("PRAIRIE:"):
345
+ action, value = content.split(":", 1)
346
+ if action == "ANIMAL":
347
+ st.session_state.prairie_players[client_id]["animal"] = value
348
+ elif action == "MOVE":
349
+ target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"])
350
+ st.session_state.prairie_players[client_id]["location"] = target
351
+ action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}"
352
+ await save_chat_entry(sender, action_msg, voice)
353
+ else:
354
+ await save_chat_entry(sender, content, voice)
355
+ await perform_arxiv_search(content, sender)
356
+ update_game_state(game_state)
357
+ except websockets.ConnectionClosed:
358
+ await broadcast_message(f"System|{username} leaves the quest!", room_id)
359
+ if client_id in st.session_state.players:
360
+ del st.session_state.players[client_id]
361
+ if client_id in st.session_state.prairie_players:
362
+ del st.session_state.prairie_players[client_id]
363
+ finally:
364
+ if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]:
365
+ del st.session_state.active_connections[room_id][client_id]
366
 
367
+ async def periodic_update():
368
+ while True:
369
+ if st.session_state.active_connections.get("quest"):
370
+ game_state = load_game_state(st.session_state.game_state_timestamp)
371
+ current_time = time.time()
372
+ inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout]
373
+ for player in inactive_players:
374
+ del game_state["players"][player]
375
+ await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest")
376
+ for username in game_state["players"]:
377
+ game_state["players"][username]["score"] += int((current_time - game_state["players"][username]["last_active"]) * 60) # $1 per second
378
+ update_game_state(game_state)
379
 
380
+ player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!"
381
+ message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}"
382
+ player_data = json.dumps(list(game_state["players"].values()))
383
+ await broadcast_message(f"MAP_UPDATE:{player_data}", "quest")
384
+
385
+ prairie_list = ", ".join([f"{p['username']} ({p['animal']})" for p in st.session_state.prairie_players.values()]) or "No animals yet!"
386
+ prairie_message = f"๐ŸŒพ Prairie Update: {prairie_list}"
387
+ prairie_data = json.dumps(list(st.session_state.prairie_players.values()))
388
+ await broadcast_message(f"PRAIRIE_UPDATE:{prairie_data}", "quest")
389
+
390
+ chat_content = await load_chat()
391
+ await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest")
392
+ await broadcast_message(f"GAME_STATE:{json.dumps(game_state)}", "quest")
393
+ await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural")
394
+ await asyncio.sleep(st.session_state.update_interval)
395
 
396
+ async def run_websocket_server():
397
+ if not st.session_state.get('server_running', False):
398
+ server = await websockets.serve(websocket_handler, '0.0.0.0', 8765)
399
+ st.session_state['server_running'] = True
400
+ asyncio.create_task(periodic_update())
401
+ await server.wait_closed()
402
 
403
+ def start_websocket_server():
404
+ loop = asyncio.new_event_loop()
405
+ asyncio.set_event_loop(loop)
406
+ loop.run_until_complete(run_websocket_server())
 
 
407
 
408
+ # ArXiv Integration
409
+ async def perform_arxiv_search(query, username):
410
+ gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern")
411
+ refs = gradio_client.predict(
412
+ query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md"
413
+ )[0]
414
+ result = f"๐Ÿ“š Ancient Rocky Knowledge:\n{refs}"
415
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username)
416
+ md_file, audio_file = await save_chat_entry(username, result, voice, True)
417
+ return md_file, audio_file
418
+
419
+ # Sidebar Functions
420
+ def update_marquee_settings_ui():
421
+ st.sidebar.markdown("### ๐ŸŽฏ Marquee Settings")
422
+ cols = st.sidebar.columns(2)
423
+ with cols[0]:
424
+ bg_color = st.color_picker("๐ŸŽจ Background", st.session_state['marquee_settings']["background"], key="bg_color_picker")
425
+ text_color = st.color_picker("โœ๏ธ Text", st.session_state['marquee_settings']["color"], key="text_color_picker")
426
+ with cols[1]:
427
+ font_size = st.slider("๐Ÿ“ Size", 10, 24, 14, key="font_size_slider")
428
+ duration = st.slider("โฑ๏ธ Speed (secs)", 1, 20, 20, key="duration_slider")
429
+ st.session_state['marquee_settings'].update({
430
+ "background": bg_color,
431
+ "color": text_color,
432
+ "font-size": f"{font_size}px",
433
+ "animationDuration": f"{duration}s"
434
+ })
435
+
436
+ def display_file_history_in_sidebar():
437
+ st.sidebar.markdown("---")
438
+ st.sidebar.markdown("### ๐Ÿ“‚ Chat Gallery")
439
+ chat_files = sorted(glob.glob("*.mp3"), key=os.path.getmtime, reverse=True)
440
+ if not chat_files:
441
+ st.sidebar.write("No chat audio files found.")
442
+ return
443
+ for audio_file in chat_files[:10]:
444
+ with st.sidebar.expander(os.path.basename(audio_file)):
445
+ st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}")
446
+ play_and_download_audio(audio_file)
447
+
448
+ def log_performance_metrics():
449
+ st.sidebar.markdown("### โฑ๏ธ Performance Metrics")
450
+ metrics = st.session_state['operation_timings']
451
+ if metrics:
452
+ total_time = sum(metrics.values())
453
+ st.sidebar.write(f"**Total Processing Time:** {total_time:.2f}s")
454
+ for operation, duration in metrics.items():
455
+ percentage = (duration / total_time) * 100
456
+ st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
457
+
458
+ # Enhanced 3D Game HTML
459
+ rocky_map_html = f"""
460
+ <!DOCTYPE html>
461
+ <html lang="en">
462
+ <head>
463
+ <meta charset="UTF-8">
464
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
465
+ <title>Rocky Mountain Quest 3D</title>
466
+ <style>
467
+ body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }}
468
+ #gameContainer {{ width: 800px; height: 600px; position: relative; }}
469
+ canvas {{ width: 100%; height: 100%; display: block; }}
470
+ .ui-container {{
471
+ position: absolute; top: 10px; left: 10px; color: white;
472
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
473
+ user-select: none;
474
+ }}
475
+ .leaderboard {{
476
+ position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white;
477
+ background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px;
478
+ text-align: center;
479
+ }}
480
+ .controls {{
481
+ position: absolute; bottom: 10px; left: 10px; color: white;
482
+ background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px;
483
+ }}
484
+ #chatBox {{
485
+ position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px;
486
+ background: rgba(0, 0, 0, 0.7); color: white; padding: 10px;
487
+ border-radius: 5px; overflow-y: auto;
488
+ }}
489
+ </style>
490
+ </head>
491
+ <body>
492
+ <div id="gameContainer">
493
+ <div class="ui-container">
494
+ <h2>{GAME_NAME}</h2>
495
+ <div id="score">Score: 0</div>
496
+ <div id="treasures">Treasures: 0</div>
497
+ <div id="timeout">Timeout: 60s</div>
498
+ </div>
499
+ <div class="leaderboard" id="leaderboard">Leaderboard</div>
500
+ <div id="chatBox"></div>
501
+ <div class="controls">
502
+ <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
503
+ <p>Chat to add world features!</p>
504
+ </div>
505
+ </div>
506
 
507
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
508
+ <script>
509
+ const playerName = "{st.session_state.username}";
510
+ let ws = new WebSocket('ws://localhost:8765');
511
+ const scene = new THREE.Scene();
512
+ const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
513
+ camera.position.set(0, 50, 50);
514
+ camera.lookAt(0, 0, 0);
515
+
516
+ const renderer = new THREE.WebGLRenderer({{ antialias: true }});
517
+ renderer.setSize(800, 600);
518
+ document.getElementById('gameContainer').appendChild(renderer.domElement);
519
+
520
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
521
+ scene.add(ambientLight);
522
+ const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
523
+ sunLight.position.set(50, 50, 50);
524
+ sunLight.castShadow = true;
525
+ scene.add(sunLight);
526
+
527
+ const groundGeometry = new THREE.PlaneGeometry(100, 100);
528
+ const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
529
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
530
+ ground.rotation.x = -Math.PI / 2;
531
+ ground.receiveShadow = true;
532
+ scene.add(ground);
533
+
534
+ let players = {{}};
535
+ const playerMeshes = {{}};
536
+ let treasures = [];
537
+ const treasureMeshes = {{}};
538
+ let worldObjects = [];
539
+ const worldObjectMeshes = {{}};
540
+ let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
541
+ let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
542
+
543
+ const shapeGeometries = {{
544
+ "sphere": new THREE.SphereGeometry(1, 16, 16),
545
+ "cube": new THREE.BoxGeometry(2, 2, 2),
546
+ "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
547
+ "cone": new THREE.ConeGeometry(1, 2, 16),
548
+ "torus": new THREE.TorusGeometry(1, 0.4, 16, 100),
549
+ "dodecahedron": new THREE.DodecahedronGeometry(1),
550
+ "octahedron": new THREE.OctahedronGeometry(1),
551
+ "tetrahedron": new THREE.TetrahedronGeometry(1),
552
+ "icosahedron": new THREE.IcosahedronGeometry(1)
553
+ }};
554
+
555
+ const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
556
+ const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
557
+ playerMesh.position.set(xPos, 1, zPos);
558
+ playerMesh.castShadow = true;
559
+ scene.add(playerMesh);
560
+ players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
561
+
562
+ function updateTreasures(treasureData) {{
563
+ treasureData.forEach(t => {{
564
+ if (!treasureMeshes[t.id]) {{
565
+ const treasure = new THREE.Mesh(
566
+ new THREE.SphereGeometry(1, 8, 8),
567
+ new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
568
+ );
569
+ treasure.position.set(t.x, 1, t.z);
570
+ treasure.castShadow = true;
571
+ treasureMeshes[t.id] = treasure;
572
+ treasures.push(treasure);
573
+ scene.add(treasure);
574
+ }} else {{
575
+ treasureMeshes[t.id].position.set(t.x, 1, t.z);
576
+ }}
577
+ }});
578
+ Object.keys(treasureMeshes).forEach(id => {{
579
+ if (!treasureData.some(t => t.id === id)) {{
580
+ scene.remove(treasureMeshes[id]);
581
+ treasures = treasures.filter(t => t !== treasureMeshes[id]);
582
+ delete treasureMeshes[id];
583
+ }}
584
+ }});
585
+ }}
586
+
587
+ function updateWorldObjects(objectData) {{
588
+ objectData.forEach(obj => {{
589
+ if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
590
+ const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
591
+ const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
592
+ const objMesh = new THREE.Mesh(geometry, material);
593
+ objMesh.position.set(obj.x, 1, obj.z);
594
+ objMesh.castShadow = true;
595
+ worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
596
+ worldObjects.push(objMesh);
597
+ scene.add(objMesh);
598
+ }}
599
+ }});
600
+ }}
601
+
602
+ document.addEventListener('keydown', (event) => {{
603
+ switch (event.code) {{
604
+ case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
605
+ case 'ArrowRight': case 'KeyD': moveRight = true; break;
606
+ case 'ArrowUp': case 'KeyW': moveUp = true; break;
607
+ case 'ArrowDown': case 'KeyS': moveDown = true; break;
608
+ case 'Space': collect = true; break;
609
+ }}
610
+ lastActive = performance.now() / 1000;
611
+ }});
612
+ document.addEventListener('keyup', (event) => {{
613
+ switch (event.code) {{
614
+ case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
615
+ case 'ArrowRight': case 'KeyD': moveRight = false; break;
616
+ case 'ArrowUp': case 'KeyW': moveUp = false; break;
617
+ case 'ArrowDown': case 'KeyS': moveDown = false; break;
618
+ case 'Space': collect = false; break;
619
+ }}
620
+ }});
621
+
622
+ function updatePlayer(delta) {{
623
+ const speed = 20;
624
+ if (moveLeft && xPos > -40) xPos -= speed * delta;
625
+ if (moveRight && xPos < 40) xPos += speed * delta;
626
+ if (moveUp && zPos > -40) zPos -= speed * delta;
627
+ if (moveDown && zPos < 40) zPos += speed * delta;
628
+ playerMesh.position.set(xPos, 1, zPos);
629
+ ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
630
+
631
+ if (collect) {{
632
+ for (let i = treasures.length - 1; i >= 0; i--) {{
633
+ if (playerMesh.position.distanceTo(treasures[i].position) < 2) {{
634
+ const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
635
+ scene.remove(treasures[i]);
636
+ treasures.splice(i, 1);
637
+ delete treasureMeshes[id];
638
+ score += 50;
639
+ treasureCount += 1;
640
+ ws.send(`${{playerName}}|SCORE:${{score}}`);
641
+ ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
642
+ }}
643
+ }}
644
+ }}
645
+
646
+ camera.position.set(xPos, 50, zPos + 50);
647
+ camera.lookAt(xPos, 0, zPos);
648
+ const timeout = 60 - (performance.now() / 1000 - lastActive);
649
+ document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
650
+ document.getElementById('score').textContent = `Score: $${{score}}`;
651
+ document.getElementById('treasures').textContent = `Treasures: ${{treasureCount}}`;
652
+ }}
653
+
654
+ function updatePlayers(playerData) {{
655
+ playerData.forEach(player => {{
656
+ if (!playerMeshes[player.username]) {{
657
+ const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
658
+ const material = new THREE.MeshPhongMaterial({{ color: player.color }});
659
+ const mesh = new THREE.Mesh(geometry, material);
660
+ mesh.castShadow = true;
661
+ scene.add(mesh);
662
+ playerMeshes[player.username] = mesh;
663
+ }}
664
+ const mesh = playerMeshes[player.username];
665
+ mesh.position.set(player.x, 1, player.z);
666
+ players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
667
+ if (player.username === playerName) {{
668
+ xPos = player.x;
669
+ zPos = player.z;
670
+ score = player.score;
671
+ treasureCount = player.treasures;
672
+ playerMesh.position.set(xPos, 1, zPos);
673
+ }}
674
+ }});
675
+ document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
676
+ const leaderboard = playerData.sort((a, b) => b.score - a.score)
677
+ .map(p => `${{p.username}}: $${{p.score}}`)
678
+ .join('<br>');
679
+ document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
680
+ }}
681
+
682
+ ws.onmessage = function(event) {{
683
+ const data = event.data;
684
+ if (data.startsWith('MAP_UPDATE:')) {{
685
+ const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
686
+ updatePlayers(playerData);
687
+ }} else if (data.startsWith('CHAT_UPDATE:')) {{
688
+ const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
689
+ const chatBox = document.getElementById('chatBox');
690
+ chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
691
+ chatBox.scrollTop = chatBox.scrollHeight;
692
+ }} else if (data.startsWith('GAME_STATE:')) {{
693
+ const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
694
+ updatePlayers(gameState.players);
695
+ updateTreasures(gameState.treasures);
696
+ updateWorldObjects(gameState.world_objects);
697
+ }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
698
+ const [sender, message] = data.split('|');
699
+ const chatBox = document.getElementById('chatBox');
700
+ chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
701
+ chatBox.scrollTop = chatBox.scrollHeight;
702
+ }}
703
+ }};
704
+
705
+ let lastTime = performance.now();
706
+ function animate() {{
707
+ requestAnimationFrame(animate);
708
+ const currentTime = performance.now();
709
+ const delta = (currentTime - lastTime) / 1000;
710
+ lastTime = currentTime;
711
+
712
+ updatePlayer(delta);
713
+ treasures.forEach(t => t.rotation.y += delta);
714
+ worldObjects.forEach(o => o.rotation.y += delta * 0.5);
715
+ renderer.render(scene, camera);
716
+ }}
717
+
718
+ animate();
719
+ </script>
720
+ </body>
721
+ </html>
722
+ """
723
+
724
+ # Main Game Loop
725
+ def main():
726
+ # Top Center Chat and Character Display
727
+ st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
728
+ message = st.text_input(f"๐Ÿ—จ๏ธ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! ๐ŸŒฒ", key="chat_input")
729
+ if st.button("๐ŸŒŸ Send"):
730
+ if message:
731
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
732
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice))
733
+ if audio_file:
734
+ play_and_download_audio(audio_file)
735
+ st.session_state.last_activity = time.time()
736
+ st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
737
+
738
+ update_marquee_settings_ui()
739
+ st.sidebar.title(f"๐ŸŽฎ {GAME_NAME}")
740
+ st.sidebar.subheader(f"๐ŸŒ„ {st.session_state.username}โ€™s Adventure - Score: ${st.session_state.score} ๐Ÿ†")
741
+ st.sidebar.write(f"๐Ÿ“œ {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}")
742
+ st.sidebar.write(f"๐Ÿ“ Location: {st.session_state.location}")
743
+ st.sidebar.write(f"๐Ÿ… Score: ${st.session_state.score}")
744
+ st.sidebar.write(f"๐ŸŽต Treasures: {st.session_state.treasures}")
745
+ st.sidebar.write(f"๐Ÿ‘ฅ Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}")
746
+
747
+ st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1)
748
  if st.sidebar.button("Reset World ๐ŸŒ"):
749
  reset_game_state()
750
+ st.session_state.game_state_timestamp = time.time()
751
+ st.rerun()
752
+
753
+ # Demo Buttons
754
+ st.sidebar.markdown("### ๐ŸŒŸ World Additions")
755
+ if st.sidebar.button("๐ŸŒฟ Add Plants"):
756
+ asyncio.run(save_chat_entry(st.session_state.username, "plants", st.session_state.tts_voice))
757
+ if st.sidebar.button("๐Ÿฆ Add Animal Flocks"):
758
+ asyncio.run(save_chat_entry(st.session_state.username, "animal flocks", st.session_state.tts_voice))
759
+ if st.sidebar.button("๐Ÿ”๏ธ Add Mountains"):
760
+ asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice))
761
+
762
+ left_col, right_col = st.columns([2, 1])
763
+
764
+ with left_col:
765
+ components.html(rocky_map_html, width=800, height=600)
766
+ chat_content = asyncio.run(load_chat())
767
+ st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
768
+ uploaded_file = st.file_uploader("Upload a File ๐Ÿ“ค", type=["txt", "md", "mp3"])
769
+ if uploaded_file:
770
+ with open(uploaded_file.name, "wb") as f:
771
+ f.write(uploaded_file.getbuffer())
772
+ game_state = load_game_state(st.session_state.game_state_timestamp)
773
+ if st.session_state.username in game_state["players"]:
774
+ game_state["players"][st.session_state.username]["score"] += 20
775
+ game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}")
776
+ update_game_state(game_state)
777
+ st.session_state.last_activity = time.time()
778
+ st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}")
779
+ mycomponent = components.declare_component("speech_component", path="./speech_component")
780
+ val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}")
781
+ if val and val != st.session_state.last_transcript:
782
+ val_stripped = val.strip().replace('\n', ' ')
783
+ if val_stripped:
784
+ voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)
785
+ st.session_state['speech_processed'] = True
786
+ md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice))
787
+ if audio_file:
788
+ play_and_download_audio(audio_file)
789
+ st.session_state.last_activity = time.time()
790
+ st.rerun()
791
+
792
+ with right_col:
793
+ st.subheader("๐ŸŒพ Prairie Map")
794
+ prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
795
+ for loc, (lat, lon) in PRAIRIE_LOCATIONS.items():
796
+ folium.Marker([lat, lon], popup=loc).add_to(prairie_map)
797
+ game_state = load_game_state(st.session_state.game_state_timestamp)
798
+ for username, player in game_state["players"].items():
799
+ folium.CircleMarker(
800
+ location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01],
801
+ radius=5,
802
+ color=f"#{player['color']:06x}",
803
+ fill=True,
804
+ fill_opacity=0.7,
805
+ popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})"
806
+ ).add_to(prairie_map)
807
+ for client_id, player in st.session_state.prairie_players.items():
808
+ folium.CircleMarker(
809
+ location=player['location'],
810
+ radius=5,
811
+ color=f"#{player['color']:06x}",
812
+ fill=True,
813
+ fill_opacity=0.7,
814
+ popup=f"{player['username']} ({player['animal']})"
815
+ ).add_to(prairie_map)
816
+ folium_static(prairie_map, width=600, height=400)
817
+
818
+ animal = st.selectbox("Choose Animal ๐Ÿพ", ["prairie_dog", "deer", "sheep", "groundhog"])
819
+ location = st.selectbox("Move to ๐Ÿ“", list(PRAIRIE_LOCATIONS.keys()))
820
+ if st.button("Move ๐Ÿšถ"):
821
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest"))
822
+ asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest"))
823
+ st.session_state.last_activity = time.time()
824
+ st.rerun()
825
+
826
+ elapsed = time.time() - st.session_state.last_update
827
+ remaining = max(0, st.session_state.update_interval - elapsed)
828
+ st.sidebar.markdown(f"โณ Next Update in: {int(remaining)}s")
829
+
830
+ if time.time() - st.session_state.last_activity > st.session_state.timeout:
831
+ st.sidebar.warning("Timed out! Refreshing in 5 seconds... โฐ")
832
+ time.sleep(5)
833
+ st.session_state.game_state_timestamp = time.time()
834
  st.rerun()
835
+ elif time.time() - st.session_state.last_update > st.session_state.auto_refresh:
836
+ st.session_state.game_state_timestamp = time.time()
837
+ st.rerun()
838
+ elif remaining <= 0:
839
+ st.session_state.last_update = time.time()
840
+ st.session_state.game_state_timestamp = time.time()
841
+ st.rerun()
842
+
843
+ if not st.session_state.get('server_running', False):
844
+ st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
845
+ st.session_state.server_task.start()
846
+
847
+ display_file_history_in_sidebar()
848
+ log_performance_metrics()
849
 
 
850
  if __name__ == "__main__":
851
+ main()