import streamlit as st import asyncio import websockets import uuid from datetime import datetime import os import random import hashlib import base64 import edge_tts import nest_asyncio import re import threading import time import json import streamlit.components.v1 as components from gradio_client import Client from streamlit_marquee import streamlit_marquee import folium from streamlit_folium import st_folium # Use st_folium instead of folium_static import glob import pytz from collections import defaultdict import pandas as pd # Patch asyncio for nesting nest_asyncio.apply() # ----------------------------- # Page and Game Configuration # ----------------------------- st.set_page_config( layout="wide", page_title="Rocky Mountain Quest 3D 🏔️🎮", page_icon="🦌", initial_sidebar_state="auto", menu_items={ 'Get Help': 'https://huggingface.co/awacke1', 'Report a bug': 'https://huggingface.co/spaces/awacke1', 'About': "Rocky Mountain Quest 3D 🏔️🎮" } ) GAME_NAME = "Rocky Mountain Quest 3D 🏔️🎮" START_LOCATION = "Trailhead Camp ⛺" EDGE_TTS_VOICES = [ {"name": "Aria 🌸", "voice": "en-US-AriaNeural", "desc": "Elegant, creative storytelling", "color": 0xFF69B4, "shape": "sphere"}, {"name": "Guy 🌟", "voice": "en-US-GuyNeural", "desc": "Authoritative, versatile", "color": 0x00FF00, "shape": "cube"}, {"name": "Jenny 🎶", "voice": "en-US-JennyNeural", "desc": "Friendly, conversational", "color": 0xFFFF00, "shape": "cylinder"}, {"name": "Sonia 🌺", "voice": "en-GB-SoniaNeural", "desc": "Bold, confident", "color": 0x0000FF, "shape": "cone"}, {"name": "Ryan 🛠️", "voice": "en-GB-RyanNeural", "desc": "Approachable, casual", "color": 0x00FFFF, "shape": "torus"}, {"name": "Natasha 🌌", "voice": "en-AU-NatashaNeural", "desc": "Sophisticated, mysterious", "color": 0x800080, "shape": "dodecahedron"}, {"name": "William 🎻", "voice": "en-AU-WilliamNeural", "desc": "Classic, scholarly", "color": 0xFFA500, "shape": "octahedron"}, {"name": "Clara 🌷", "voice": "en-CA-ClaraNeural", "desc": "Cheerful, empathetic", "color": 0xFF4500, "shape": "tetrahedron"}, {"name": "Liam 🌟", "voice": "en-CA-LiamNeural", "desc": "Energetic, engaging", "color": 0xFFD700, "shape": "icosahedron"} ] FILE_EMOJIS = {"md": "📜", "mp3": "🎵"} WORLD_OBJECTS = [ {"type": "🌿 Plants", "emoji": "🌱🌿🌾", "color": 0x228B22, "shape": "cylinder", "count": 10}, {"type": "🐦 Animal Flocks", "emoji": "🕊️🐦🐤", "color": 0x87CEEB, "shape": "sphere", "count": 5}, {"type": "🏔️ Mountains", "emoji": "🏔️⛰️", "color": 0x808080, "shape": "cone", "count": 3}, {"type": "🌳 Trees", "emoji": "🌲🌳🌴", "color": 0x006400, "shape": "cylinder", "count": 8}, {"type": "🐾 Animals", "emoji": "🐻🐺🦌", "color": 0x8B4513, "shape": "cube", "count": 6} ] PRAIRIE_LOCATIONS = { "Deadwood, SD": (44.3769, -103.7298), "Wind Cave National Park": (43.6047, -103.4798), "Wyoming Spring Creek": (41.6666, -106.6666) } # ----------------------------- # Directories and File Paths # ----------------------------- for d in ["chat_logs", "audio_logs"]: os.makedirs(d, exist_ok=True) CHAT_DIR = "chat_logs" AUDIO_DIR = "audio_logs" STATE_FILE = "user_state.txt" CHAT_FILE = os.path.join(CHAT_DIR, "quest_log.md") GAME_STATE_FILE = "game_state.json" # ----------------------------- # Utility Functions # ----------------------------- def format_timestamp(username=""): now = datetime.now(pytz.timezone('US/Central')).strftime("%m%d%Y-%I%M-%p") return f"{now}-by-{username}" def clean_text_for_tts(text): return re.sub(r'[#*!\[\]]+', '', ' '.join(text.split()))[:200] or "No text" def generate_filename(prompt, username, file_type="md"): timestamp = format_timestamp(username) cleaned_prompt = clean_text_for_tts(prompt)[:50].replace(" ", "_") return f"{cleaned_prompt}_{timestamp}.{file_type}" def create_file(prompt, username, file_type="md"): filename = generate_filename(prompt, username, file_type) with open(filename, 'w', encoding='utf-8') as f: f.write(prompt) return filename def get_download_link(file, file_type="mp3"): with open(file, "rb") as f: b64 = base64.b64encode(f.read()).decode() mime_types = {"mp3": "audio/mpeg", "md": "text/markdown"} return f'{FILE_EMOJIS.get(file_type, "📥")} {os.path.basename(file)}' def save_username(username): with open(STATE_FILE, 'w') as f: f.write(username) def load_username(): if os.path.exists(STATE_FILE): with open(STATE_FILE, 'r') as f: return f.read().strip() return None # ----------------------------- # Game State Functions # ----------------------------- @st.cache_resource def load_game_state(_timestamp): if os.path.exists(GAME_STATE_FILE): with open(GAME_STATE_FILE, 'r') as f: state = json.load(f) else: state = { "players": {}, "treasures": [ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)} for _ in range(5) ], "world_objects": [], "history": [] } with open(GAME_STATE_FILE, 'w') as f: json.dump(state, f) return state def update_game_state(state): state["timestamp"] = os.path.getmtime(GAME_STATE_FILE) if os.path.exists(GAME_STATE_FILE) else time.time() with open(GAME_STATE_FILE, 'w') as f: json.dump(state, f) load_game_state.clear() return load_game_state(time.time()) def reset_game_state(): state = { "players": {}, "treasures": [ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)} for _ in range(5) ], "world_objects": [], "history": [] } return update_game_state(state) # ----------------------------- # PlayerAgent Class # ----------------------------- class PlayerAgent: def __init__(self, username, char_data): self.username = username self.x = random.uniform(-20, 20) self.z = random.uniform(-40, 40) self.color = char_data["color"] self.shape = char_data["shape"] self.score = 0 self.treasures = 0 self.last_active = time.time() self.voice = char_data["voice"] def to_dict(self): return { "username": self.username, "x": self.x, "z": self.z, "color": self.color, "shape": self.shape, "score": self.score, "treasures": self.treasures, "last_active": self.last_active } def update_from_message(self, message): if '|' in message: _, content = message.split('|', 1) if content.startswith("MOVE:"): _, x, z = content.split(":") self.x, self.z = float(x), float(z) self.last_active = time.time() elif content.startswith("SCORE:"): self.score = int(content.split(":")[1]) self.last_active = time.time() elif content.startswith("TREASURE:"): self.treasures = int(content.split(":")[1]) self.last_active = time.time() # ----------------------------- # Audio and Chat Processing # ----------------------------- async def async_edge_tts_generate(text, voice, username): cache_key = f"{text[:100]}_{voice}" if cache_key in st.session_state['audio_cache']: return st.session_state['audio_cache'][cache_key] text = clean_text_for_tts(text) filename = generate_filename(text, username, "mp3") communicate = edge_tts.Communicate(text, voice) await communicate.save(filename) if os.path.exists(filename) and os.path.getsize(filename) > 0: st.session_state['audio_cache'][cache_key] = filename return filename return None def play_and_download_audio(file_path): if file_path and os.path.exists(file_path): st.audio(file_path, start_time=0) st.markdown(get_download_link(file_path), unsafe_allow_html=True) def log_chat_history(username, message, is_markdown=False): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") entry = f"[{timestamp}] {username}: {message}" if not is_markdown else f"[{timestamp}] {username}:\n```markdown\n{message}\n```" md_file = create_file(entry, username, "md") with open(CHAT_FILE, 'a') as f: f.write(f"{entry}\n") return entry async def generate_chat_audio(username, message, voice): audio_file = await async_edge_tts_generate(message, voice, username) return audio_file async def save_chat_entry(username, message, voice, is_markdown=False): if not message.strip() or message == st.session_state.get('last_transcript', ''): return None, None entry = log_chat_history(username, message, is_markdown) audio_file = await generate_chat_audio(username, message, voice) await broadcast_message(f"{username}|{message}", "quest") st.session_state.chat_history.append({"username": username, "message": message, "audio": audio_file}) st.session_state.last_transcript = message game_state = load_game_state(st.session_state.game_state_timestamp) if username in game_state["players"]: game_state["players"][username]["score"] += 10 game_state["players"][username]["treasures"] += 1 game_state["players"][username]["last_active"] = time.time() game_state["history"].append(entry) if message.lower() in ["plants", "animal flocks", "mountains", "trees", "animals"]: obj_type = next(o for o in WORLD_OBJECTS if message.lower() in o["type"].lower()) for _ in range(obj_type["count"]): game_state["world_objects"].append({ "type": obj_type["type"], "emoji": random.choice(obj_type["emoji"].split()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40), "color": obj_type["color"], "shape": obj_type["shape"] }) update_game_state(game_state) return entry, audio_file async def load_chat(): if not os.path.exists(CHAT_FILE): with open(CHAT_FILE, 'a') as f: f.write(f"# {GAME_NAME} Log\n\nThe adventure begins at {START_LOCATION}! 🏔️\n") with open(CHAT_FILE, 'r') as f: content = f.read().strip() return content.split('\n') # ----------------------------- # Broadcast Message Function # ----------------------------- async def broadcast_message(message, room_id): if room_id in st.session_state.active_connections: disconnected = [] for client_id, ws in st.session_state.active_connections[room_id].items(): try: await ws.send(message) except websockets.ConnectionClosed: disconnected.append(client_id) for client_id in disconnected: if client_id in st.session_state.active_connections[room_id]: del st.session_state.active_connections[room_id][client_id] # ----------------------------- # Session State Initialization # ----------------------------- # Define global variables for player attributes. player_color = None player_shape = None def init_session_state(): defaults = { 'server_running': False, 'server_task': None, 'active_connections': {}, 'chat_history': [], 'audio_cache': {}, 'last_transcript': "", 'username': None, 'score': 0, 'treasures': 0, 'location': START_LOCATION, 'speech_processed': False, 'players': {}, 'last_update': time.time(), 'update_interval': 1, 'x_pos': 0, 'z_pos': 0, 'move_left': False, 'move_right': False, 'move_up': False, 'move_down': False, 'prairie_players': {}, 'last_chat_update': 0, 'game_state_timestamp': time.time(), 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time(), 'marquee_settings': {"background": "#1E1E1E", "color": "#FFFFFF", "font-size": "14px", "animationDuration": "20s", "width": "100%", "lineHeight": "35px"}, 'operation_timings': {}, 'performance_metrics': defaultdict(list), 'download_link_cache': {} } for k, v in defaults.items(): if k not in st.session_state: st.session_state[k] = v if st.session_state.username is None: char = random.choice(EDGE_TTS_VOICES) st.session_state.username = char["name"] st.session_state.tts_voice = char["voice"] asyncio.run(save_chat_entry(st.session_state.username, f"🗺️ Welcome to the Rocky Mountain Quest, {st.session_state.username}!", char["voice"])) global player_color, player_shape if player_color is None or player_shape is None: char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) player_color = char["color"] player_shape = char["shape"] game_state = load_game_state(st.session_state.game_state_timestamp) if st.session_state.username not in game_state["players"]: char = next(c for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) agent = PlayerAgent(st.session_state.username, char) game_state["players"][st.session_state.username] = agent.to_dict() update_game_state(game_state) init_session_state() # ----------------------------- # WebSocket and Multiplayer Functions # ----------------------------- async def websocket_handler(websocket, path): client_id = str(uuid.uuid4()) room_id = "quest" if room_id not in st.session_state.active_connections: st.session_state.active_connections[room_id] = {} st.session_state.active_connections[room_id][client_id] = websocket username = st.session_state.username game_state = load_game_state(st.session_state.game_state_timestamp) if "prairie" in path: char = next(c for c in EDGE_TTS_VOICES if c["name"] == username) st.session_state.prairie_players[client_id] = { "username": username, "animal": "prairie_dog", "location": PRAIRIE_LOCATIONS["Deadwood, SD"], "color": char["color"] } await broadcast_message(f"System|{username} joins the prairie!", room_id) else: if username not in game_state["players"]: char = next(c for c in EDGE_TTS_VOICES if c["name"] == username) agent = PlayerAgent(username, char) game_state["players"][username] = agent.to_dict() update_game_state(game_state) st.session_state.players[client_id] = game_state["players"][username] await broadcast_message(f"System|{username} joins the quest!", room_id) try: async for message in websocket: if '|' in message: sender, content = message.split('|', 1) voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == sender) game_state = load_game_state(st.session_state.game_state_timestamp) agent = PlayerAgent(sender, next(c for c in EDGE_TTS_VOICES if c["name"] == sender)) agent.update_from_message(f"{sender}|{content}") if sender in game_state["players"]: game_state["players"][sender] = agent.to_dict() if content.startswith("PRAIRIE:"): action, value = content.split(":", 1) if action == "ANIMAL": st.session_state.prairie_players[client_id]["animal"] = value elif action == "MOVE": target = PRAIRIE_LOCATIONS.get(value, PRAIRIE_LOCATIONS["Deadwood, SD"]) st.session_state.prairie_players[client_id]["location"] = target action_msg = f"{sender} ({st.session_state.prairie_players[client_id]['animal']}) moves to {value}" await save_chat_entry(sender, action_msg, voice) else: await save_chat_entry(sender, content, voice) await perform_arxiv_search(content, sender) update_game_state(game_state) except websockets.ConnectionClosed: await broadcast_message(f"System|{username} leaves the quest!", room_id) if client_id in st.session_state.players: del st.session_state.players[client_id] if client_id in st.session_state.prairie_players: del st.session_state.prairie_players[client_id] finally: if room_id in st.session_state.active_connections and client_id in st.session_state.active_connections[room_id]: del st.session_state.active_connections[room_id][client_id] async def periodic_update(): while True: if st.session_state.active_connections.get("quest"): game_state = load_game_state(st.session_state.game_state_timestamp) current_time = time.time() inactive_players = [p for p, data in game_state["players"].items() if current_time - data["last_active"] > st.session_state.timeout] for player in inactive_players: del game_state["players"][player] await broadcast_message(f"System|{player} timed out after 60 seconds!", "quest") for username in game_state["players"]: game_state["players"][username]["score"] += int((current_time - game_state["players"][username]["last_active"]) * 60) update_game_state(game_state) player_list = ", ".join([p for p in game_state["players"].keys()]) or "No adventurers yet!" message = f"📢 Quest Update: Active Adventurers - {player_list}" player_data = json.dumps(list(game_state["players"].values())) await broadcast_message(f"MAP_UPDATE:{player_data}", "quest") prairie_list = ", ".join([f"{p['username']} ({p['animal']})" for p in st.session_state.prairie_players.values()]) or "No animals yet!" prairie_message = f"🌾 Prairie Update: {prairie_list}" prairie_data = json.dumps(list(st.session_state.prairie_players.values())) await broadcast_message(f"PRAIRIE_UPDATE:{prairie_data}", "quest") chat_content = await load_chat() await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "quest") await broadcast_message(f"GAME_STATE:{json.dumps(game_state)}", "quest") await save_chat_entry("System", f"{message}\n{prairie_message}", "en-US-AriaNeural") await asyncio.sleep(st.session_state.update_interval) async def run_websocket_server(): if not st.session_state.get('server_running', False): server = await websockets.serve(websocket_handler, '0.0.0.0', 8765) st.session_state['server_running'] = True asyncio.create_task(periodic_update()) await server.wait_closed() def start_websocket_server(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(run_websocket_server()) async def perform_arxiv_search(query, username): gradio_client = Client("awacke1/Arxiv-Paper-Search-And-QA-RAG-Pattern") refs = gradio_client.predict( query, 5, "Semantic Search", "mistralai/Mixtral-8x7B-Instruct-v0.1", api_name="/update_with_rag_md" )[0] result = f"📚 Ancient Rocky Knowledge:\n{refs}" voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == username) entry, audio_file = await save_chat_entry(username, result, voice, True) return entry, audio_file # ----------------------------- # Sidebar Functions # ----------------------------- def update_marquee_settings_ui(): st.sidebar.markdown("### 🎯 Marquee Settings") cols = st.sidebar.columns(2) with cols[0]: bg_color = st.color_picker("🎨 Background", st.session_state['marquee_settings']["background"], key="bg_color_picker") text_color = st.color_picker("✍️ Text", st.session_state['marquee_settings']["color"], key="text_color_picker") with cols[1]: font_size = st.slider("📏 Size", 10, 24, 14, key="font_size_slider") duration = st.slider("⏱️ Speed (secs)", 1, 20, 20, key="duration_slider") st.session_state['marquee_settings'].update({ "background": bg_color, "color": text_color, "font-size": f"{font_size}px", "animationDuration": f"{duration}s" }) def display_file_history_in_sidebar(): st.sidebar.markdown("---") st.sidebar.markdown("### 📂 Chat Gallery") chat_files = sorted(glob.glob("*.mp3"), key=os.path.getmtime, reverse=True) if not chat_files: st.sidebar.write("No chat audio files found.") return for audio_file in chat_files[:10]: with st.sidebar.expander(os.path.basename(audio_file)): st.write(f"**Said:** {os.path.splitext(os.path.basename(audio_file))[0].split('_')[0]}") play_and_download_audio(audio_file) def log_performance_metrics(): st.sidebar.markdown("### ⏱️ Performance Metrics") metrics = st.session_state.get('operation_timings', {}) if metrics: total_time = sum(metrics.values()) st.sidebar.write(f"**Total Processing Time:** {total_time:.2f}s") for operation, duration in metrics.items(): percentage = (duration / total_time) * 100 st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)") # ----------------------------- # Enhanced 3D World HTML # ----------------------------- player_color = next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) player_shape = next(c["shape"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) rocky_map_html = f""" {GAME_NAME}

{GAME_NAME}

Score: 0
Treasures: 0
Timeout: 60s
Leaderboard

Controls: WASD/Arrows to move, Space to collect treasure

Chat to add world features!

""" # ----------------------------- # Main Application # ----------------------------- def main(): st.markdown(f"

Welcome, {st.session_state.username}!

", unsafe_allow_html=True) # Chat Input Section message = st.text_input(f"🗨️ Chat as {st.session_state.username}:", placeholder="Type to chat or add world features! 🌲", key="chat_input") if st.button("🌟 Send"): if message: voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) entry, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice)) if audio_file: play_and_download_audio(audio_file) st.session_state.last_activity = time.time() st.success(f"🌄 +10 points! New Score: ${st.session_state.score}") # Display Quest Log as a code block (with Python formatting) chat_content = asyncio.run(load_chat()) st.code("\n".join(chat_content[-10:]), language="python") update_marquee_settings_ui() st.sidebar.title(f"🎮 {GAME_NAME}") st.sidebar.subheader(f"🌄 {st.session_state.username}’s Adventure - Score: ${st.session_state.score} 🏆") st.sidebar.write(f"📜 {next(c['desc'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}") st.sidebar.write(f"📍 Location: {st.session_state.location}") st.sidebar.write(f"🏅 Score: ${st.session_state.score}") st.sidebar.write(f"🎵 Treasures: {st.session_state.treasures}") st.sidebar.write(f"👥 Players: {', '.join([p['username'] for p in st.session_state.players.values()]) or 'None'}") st.session_state.update_interval = st.sidebar.slider("Refresh Interval (seconds)", 1, 10, 1, step=1) if st.sidebar.button("Reset World 🌍"): reset_game_state() st.session_state.game_state_timestamp = time.time() st.rerun() st.sidebar.markdown("### 🌟 World Additions") if st.sidebar.button("🌿 Add Plants"): asyncio.run(save_chat_entry(st.session_state.username, "plants", st.session_state.tts_voice)) if st.sidebar.button("🐦 Add Animal Flocks"): asyncio.run(save_chat_entry(st.session_state.username, "animal flocks", st.session_state.tts_voice)) if st.sidebar.button("🏔️ Add Mountains"): asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice)) left_col, right_col = st.columns([2, 1]) with left_col: components.html(rocky_map_html, width=800, height=600) uploaded_file = st.file_uploader("Upload a File 📤", type=["txt", "md", "mp3"]) if uploaded_file: with open(uploaded_file.name, "wb") as f: f.write(uploaded_file.getbuffer()) game_state = load_game_state(st.session_state.game_state_timestamp) if st.session_state.username in game_state["players"]: game_state["players"][st.session_state.username]["score"] += 20 game_state["history"].append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {st.session_state.username}: Uploaded {uploaded_file.name}") update_game_state(game_state) st.session_state.last_activity = time.time() st.success(f"File uploaded! +20 points! New Score: ${st.session_state.score}") mycomponent = components.declare_component("speech_component", path="./speech_component") val = mycomponent(my_input_value="", key=f"speech_{st.session_state.get('speech_processed', False)}") if val and val != st.session_state.last_transcript: val_stripped = val.strip().replace('\n', ' ') if val_stripped: voice = next(c["voice"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username) st.session_state['speech_processed'] = True entry, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice)) if audio_file: play_and_download_audio(audio_file) st.session_state.last_activity = time.time() st.rerun() with right_col: st.subheader("🌾 Prairie Map") prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron") for loc, (lat, lon) in PRAIRIE_LOCATIONS.items(): folium.Marker([lat, lon], popup=loc).add_to(prairie_map) game_state = load_game_state(st.session_state.game_state_timestamp) for username, player in game_state["players"].items(): folium.CircleMarker( location=[44.0 + player["x"] * 0.01, -103.0 + player["z"] * 0.01], radius=5, color=f"#{player['color']:06x}", fill=True, fill_opacity=0.7, popup=f"{username} (Score: ${player['score']}, Treasures: {player['treasures']})" ).add_to(prairie_map) for client_id, player in st.session_state.prairie_players.items(): folium.CircleMarker( location=player['location'], radius=5, color=f"#{player['color']:06x}", fill=True, fill_opacity=0.7, popup=f"{player['username']} ({player['animal']})" ).add_to(prairie_map) for obj in game_state["world_objects"]: lat = 44.0 + obj["x"] * 0.01 lon = -103.0 + obj["z"] * 0.01 folium.Marker( location=[lat, lon], popup=f"{obj['emoji']} {obj['type']}", icon=folium.DivIcon(html=f"
{obj['emoji']}
") ).add_to(prairie_map) st_folium(prairie_map, width=600, height=400) animal = st.selectbox("Choose Animal 🐾", ["prairie_dog", "deer", "sheep", "groundhog"]) location = st.selectbox("Move to 📍", list(PRAIRIE_LOCATIONS.keys())) if st.button("Move 🚶"): asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:ANIMAL:{animal}", "quest")) asyncio.run(broadcast_message(f"{st.session_state.username}|PRAIRIE:MOVE:{location}", "quest")) st.session_state.last_activity = time.time() st.rerun() elapsed = time.time() - st.session_state.last_update remaining = max(0, st.session_state.update_interval - elapsed) st.sidebar.markdown(f"⏳ Next Update in: {int(remaining)}s") if time.time() - st.session_state.last_activity > st.session_state.timeout: st.sidebar.warning("Timed out! Refreshing in 5 seconds... ⏰") time.sleep(5) st.session_state.game_state_timestamp = time.time() st.rerun() elif time.time() - st.session_state.last_update > st.session_state.auto_refresh: st.session_state.game_state_timestamp = time.time() st.rerun() elif remaining <= 0: st.session_state.last_update = time.time() st.session_state.game_state_timestamp = time.time() st.rerun() if not st.session_state.get('server_running', False): st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True) st.session_state.server_task.start() display_file_history_in_sidebar() log_performance_metrics() if __name__ == "__main__": main()