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 = { "Trailblazer Tim ๐ŸŒ„": {"voice": "en-US-GuyNeural", "desc": "Fearless hiker seeking epic trails!", "color": 0x00ff00}, "Meme Queen Mia ๐Ÿ˜‚": {"voice": "en-US-JennyNeural", "desc": "Spreads laughs with wild memes!", "color": 0xff00ff}, "Elk Whisperer Eve ๐ŸฆŒ": {"voice": "en-GB-SoniaNeural", "desc": "Talks to wildlife, loves nature!", "color": 0x0000ff}, "Tech Titan Tara ๐Ÿ’พ": {"voice": "en-AU-NatashaNeural", "desc": "Codes her way through the Rockies!", "color": 0xffff00}, "Ski Guru Sam โ›ท๏ธ": {"voice": "en-CA-ClaraNeural", "desc": "Shreds slopes, lives for snow!", "color": 0xffa500}, "Cosmic Camper Cal ๐ŸŒ ": {"voice": "en-US-AriaNeural", "desc": "Stargazes and tells epic tales!", "color": 0x800080}, "Rasta Ranger Rick ๐Ÿƒ": {"voice": "en-GB-RyanNeural", "desc": "Chills with natureโ€™s vibes!", "color": 0x00ffff}, "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 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") # 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 st.session_state.score += 10 st.session_state.treasures += 1 st.session_state.last_chat_update = time.time() 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 } 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: saved_username = load_username() if saved_username and saved_username in CHARACTERS: st.session_state.username = saved_username else: st.session_state.username = random.choice(list(CHARACTERS.keys())) asyncio.run(save_chat_entry(st.session_state.username, "๐Ÿ—บ๏ธ Begins the Rocky Mountain Quest!", CHARACTERS[st.session_state.username]["voice"])) save_username(st.session_state.username) 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 if "prairie" in path: st.session_state.prairie_players[client_id] = { "username": username, "animal": "prairie_dog", "location": PRAIRIE_LOCATIONS["Deadwood, SD"], "color": CHARACTERS[username]["color"] } await broadcast_message(f"System|{username} joins the prairie!", room_id) else: st.session_state.players[client_id] = { "username": username, "x": random.uniform(-20, 20), "z": random.uniform(-50, 50), "color": CHARACTERS[username]["color"], "score": 0, "treasures": 0 } 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 = CHARACTERS.get(sender, {"voice": "en-US-AriaNeural"})["voice"] if content.startswith("MOVE:"): _, x, z = content.split(":") st.session_state.players[client_id]["x"] = float(x) st.session_state.players[client_id]["z"] = float(z) 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) elif content.startswith("SCORE:"): st.session_state.players[client_id]["score"] = int(content.split(":")[1]) elif content.startswith("TREASURE:"): st.session_state.players[client_id]["treasures"] = int(content.split(":")[1]) 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"): player_list = ", ".join([p["username"] for p in st.session_state.players.values()]) or "No adventurers yet!" message = f"๐Ÿ“ข Quest Update: Active Adventurers - {player_list}" player_data = json.dumps(list(st.session_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") # Multicast chat update every second chat_content = await load_chat() await broadcast_message(f"CHAT_UPDATE:{json.dumps(chat_content[-10:])}", "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 = CHARACTERS[username]["voice"] 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""" Rocky Mountain Quest 3D
Players: 1
Score: 0
Treasures: 0
WASD/Arrows to move, Space to collect treasure
""" # Main Game Loop def main(): 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"๐Ÿ“œ {CHARACTERS[st.session_state.username]['desc']}") 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'}") new_username = st.sidebar.selectbox("๐Ÿง™โ€โ™‚๏ธ Choose Your Hero", list(CHARACTERS.keys()), index=list(CHARACTERS.keys()).index(st.session_state.username)) if new_username != st.session_state.username: asyncio.run(save_chat_entry(st.session_state.username, f"๐Ÿ”„ Transforms into {new_username}!", CHARACTERS[st.session_state.username]["voice"])) st.session_state.username = new_username save_username(st.session_state.username) st.rerun() left_col, right_col = st.columns([2, 1]) with left_col: components.html(rocky_map_html, width=800, height=600) chat_content = asyncio.run(load_chat()) st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True) message = st.text_input(f"๐Ÿ—จ๏ธ {st.session_state.username} says:", placeholder="Speak or type to chat! ๐ŸŒฒ") if st.button("๐ŸŒŸ Send & Chat ๐ŸŽค"): if message: voice = CHARACTERS[st.session_state.username]["voice"] md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, message, voice)) if audio_file: play_and_download_audio(audio_file) st.success(f"๐ŸŒ„ +10 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 = CHARACTERS.get(st.session_state.username, {"voice": "en-US-AriaNeural"})["voice"] st.session_state['speech_processed'] = True md_file, audio_file = asyncio.run(save_chat_entry(st.session_state.username, val_stripped, voice)) if audio_file: play_and_download_audio(audio_file) 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) 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) folium_static(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.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 remaining <= 0: st.session_state.last_update = 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() if __name__ == "__main__": main()