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 folium_static # Patch asyncio for nesting nest_asyncio.apply() # Page Config st.set_page_config( layout="wide", page_title="Rocky Mountain Quest 3D ๐๏ธ๐ฎ", page_icon="๐ฆ" ) # Game Config GAME_NAME = "Rocky Mountain Quest 3D ๐๏ธ๐ฎ" START_LOCATION = "Trailhead Camp โบ" CHARACTERS = [ {"name": "Trailblazer Tim ๐", "voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00}, {"name": "Meme Queen Mia ๐", "voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff}, {"name": "Elk Whisperer Eve ๐ฆ", "voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff}, {"name": "Tech Titan Tara ๐พ", "voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00}, {"name": "Ski Guru Sam โท๏ธ", "voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500}, {"name": "Cosmic Camper Cal ๐ ", "voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080}, {"name": "Rasta Ranger Rick ๐", "voice": "en-GB-RyanNeural", "desc": "Chills with natureโs vibes!", "color": 0x00ffff}, {"name": "Boulder Bro Ben ๐ชจ", "voice": "en-AU-WilliamNeural", "desc": "Climbs rocks, bro-style!", "color": 0xff4500} ] FILE_EMOJIS = {"md": "๐", "mp3": "๐ต"} # Prairie Simulator Locations 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 Files 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" # Cached Game State as "ML Model" using st.cache_resource @st.cache_resource def load_game_state(_timestamp): """Load or initialize the game state, treated as a cached resource.""" 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)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)} ], "history": [] } with open(GAME_STATE_FILE, 'w') as f: json.dump(state, f) return state def update_game_state(state): """Update the game state and persist to file.""" with open(GAME_STATE_FILE, 'w') as f: json.dump(state, f) load_game_state.clear() # Clear cache to force reload return load_game_state(time.time()) def reset_game_state(): """Reset the game state to initial conditions.""" state = { "players": {}, "treasures": [ {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)}, {"id": str(uuid.uuid4()), "x": random.uniform(-40, 40), "z": random.uniform(-40, 40)} ], "history": [] } return update_game_state(state) # Helpers def format_timestamp(username=""): now = datetime.now().strftime("%Y%m%d_%H%M%S") 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) hash_val = hashlib.md5(prompt.encode()).hexdigest()[:8] return f"{timestamp}-{hash_val}.{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 # Audio 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 = f"{AUDIO_DIR}/{format_timestamp(username)}-{hashlib.md5(text.encode()).hexdigest()[:8]}.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) st.markdown(get_download_link(file_path), unsafe_allow_html=True) # WebSocket Broadcast 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] # Chat and Quest Log 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 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") audio_file = await async_edge_tts_generate(message, voice, username) await broadcast_message(f"{username}|{message}", "quest") st.session_state.chat_history.append(entry) st.session_state.last_transcript = message game_state = load_game_state(st.session_state.game_state_timestamp) game_state["history"].append(entry) 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() update_game_state(game_state) return md_file, 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') # Session State Init 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(), 'name_index': 0, 'timeout': 60, 'auto_refresh': 30, 'last_activity': time.time() } 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: st.session_state.name_index = (st.session_state.name_index + 1) % len(CHARACTERS) st.session_state.username = CHARACTERS[st.session_state.name_index]["name"] asyncio.run(save_chat_entry(st.session_state.username, "๐บ๏ธ Joins the Rocky Mountain Quest!", CHARACTERS[st.session_state.name_index]["voice"])) save_username(st.session_state.username) game_state = load_game_state(st.session_state.game_state_timestamp) if st.session_state.username not in game_state["players"]: game_state["players"][st.session_state.username] = { "x": random.uniform(-20, 20), "z": random.uniform(-40, 40), "color": CHARACTERS[st.session_state.name_index]["color"], "score": 0, "treasures": 0, "last_active": time.time() } update_game_state(game_state) init_session_state() # WebSocket for Multiplayer 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: st.session_state.prairie_players[client_id] = { "username": username, "animal": "prairie_dog", "location": PRAIRIE_LOCATIONS["Deadwood, SD"], "color": CHARACTERS[st.session_state.name_index]["color"] } await broadcast_message(f"System|{username} joins the prairie!", room_id) else: if username not in game_state["players"]: game_state["players"][username] = { "x": random.uniform(-20, 20), "z": random.uniform(-40, 40), "color": CHARACTERS[st.session_state.name_index]["color"], "score": 0, "treasures": 0, "last_active": time.time() } 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 CHARACTERS if c["name"] == sender) game_state = load_game_state(st.session_state.game_state_timestamp) if content.startswith("MOVE:"): _, x, z = content.split(":") x, z = float(x), float(z) st.session_state.players[client_id]["x"] = x st.session_state.players[client_id]["z"] = z game_state["players"][sender]["x"] = x game_state["players"][sender]["z"] = z game_state["players"][sender]["last_active"] = time.time() update_game_state(game_state) elif content.startswith("SCORE:"): score = int(content.split(":")[1]) st.session_state.players[client_id]["score"] = score game_state["players"][sender]["score"] = score game_state["players"][sender]["last_active"] = time.time() update_game_state(game_state) elif content.startswith("TREASURE:"): treasures = int(content.split(":")[1]) st.session_state.players[client_id]["treasures"] = treasures game_state["players"][sender]["treasures"] = treasures game_state["players"][sender]["last_active"] = time.time() for i, t in enumerate(game_state["treasures"]): if abs(st.session_state.players[client_id]["x"] - t["x"]) < 2 and \ abs(st.session_state.players[client_id]["z"] - t["z"]) < 2: game_state["treasures"].pop(i) break update_game_state(game_state) elif 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) 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) # Remove inactive players (timeout after 60 seconds) 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") 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()) # ArXiv Integration 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 CHARACTERS if c["name"] == username) md_file, audio_file = await save_chat_entry(username, result, voice, True) return md_file, audio_file # Enhanced 3D Game HTML rocky_map_html = f"""