Spaces:
Running
Running
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'<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>' | |
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 | |
# ----------------------------- | |
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""" | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>{GAME_NAME}</title> | |
<style> | |
body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; background: #000; }} | |
#gameContainer {{ width: 800px; height: 600px; position: relative; }} | |
canvas {{ width: 100%; height: 100%; display: block; }} | |
.ui-container {{ | |
position: absolute; top: 10px; left: 10px; color: white; | |
background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; | |
user-select: none; | |
}} | |
.leaderboard {{ | |
position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: white; | |
background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px; | |
text-align: center; | |
}} | |
.controls {{ | |
position: absolute; bottom: 10px; left: 10px; color: white; | |
background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; | |
}} | |
#chatBox {{ | |
position: absolute; bottom: 60px; left: 10px; width: 300px; height: 150px; | |
background: rgba(0, 0, 0, 0.7); color: white; padding: 10px; | |
border-radius: 5px; overflow-y: auto; | |
}} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<div class="ui-container"> | |
<h2>{GAME_NAME}</h2> | |
<div id="score">Score: 0</div> | |
<div id="treasures">Treasures: 0</div> | |
<div id="timeout">Timeout: 60s</div> | |
</div> | |
<div class="leaderboard" id="leaderboard">Leaderboard</div> | |
<div id="chatBox"></div> | |
<div class="controls"> | |
<p>Controls: WASD/Arrows to move, Space to collect treasure</p> | |
<p>Chat to add world features!</p> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
const playerName = "{st.session_state.username}"; | |
let ws = new WebSocket('ws://localhost:8765'); | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, 800/600, 0.1, 1000); | |
camera.position.set(0, 50, 50); | |
camera.lookAt(0, 0, 0); | |
const renderer = new THREE.WebGLRenderer({{ antialias: true }}); | |
renderer.setSize(800, 600); | |
document.getElementById('gameContainer').appendChild(renderer.domElement); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); | |
scene.add(ambientLight); | |
const sunLight = new THREE.DirectionalLight(0xffddaa, 1); | |
sunLight.position.set(50, 50, 50); | |
sunLight.castShadow = true; | |
scene.add(sunLight); | |
const groundGeometry = new THREE.PlaneGeometry(100, 100); | |
const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI/2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
let velocity = new THREE.Vector2(0, 0); | |
const acceleration = 50; | |
const friction = 5; | |
let xPos = 0, zPos = 0; | |
const shapeGeometries = {{ | |
"sphere": new THREE.SphereGeometry(1, 16, 16), | |
"cube": new THREE.BoxGeometry(2, 2, 2), | |
"cylinder": new THREE.CylinderGeometry(1, 1, 2, 16), | |
"cone": new THREE.ConeGeometry(1, 2, 16), | |
"torus": new THREE.TorusGeometry(1, 0.4, 16, 100), | |
"dodecahedron": new THREE.DodecahedronGeometry(1), | |
"octahedron": new THREE.OctahedronGeometry(1), | |
"tetrahedron": new THREE.TetrahedronGeometry(1), | |
"icosahedron": new THREE.IcosahedronGeometry(1) | |
}}; | |
const playerMaterial = new THREE.MeshPhongMaterial({{ color: {player_color} }}); | |
const playerMesh = new THREE.Mesh(shapeGeometries["{player_shape}"], playerMaterial); | |
playerMesh.position.set(xPos, 1, zPos); | |
playerMesh.castShadow = true; | |
scene.add(playerMesh); | |
let score = 0, treasureCount = 0; | |
let lastActive = performance.now()/1000; | |
let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false; | |
document.addEventListener('keydown', (event) => {{ | |
switch(event.code) {{ | |
case 'ArrowLeft': case 'KeyA': moveLeft = true; break; | |
case 'ArrowRight': case 'KeyD': moveRight = true; break; | |
case 'ArrowUp': case 'KeyW': moveUp = true; break; | |
case 'ArrowDown': case 'KeyS': moveDown = true; break; | |
case 'Space': collect = true; break; | |
}} | |
lastActive = performance.now()/1000; | |
}}); | |
document.addEventListener('keyup', (event) => {{ | |
switch(event.code) {{ | |
case 'ArrowLeft': case 'KeyA': moveLeft = false; break; | |
case 'ArrowRight': case 'KeyD': moveRight = false; break; | |
case 'ArrowUp': case 'KeyW': moveUp = false; break; | |
case 'ArrowDown': case 'KeyS': moveDown = false; break; | |
case 'Space': collect = false; break; | |
}} | |
}}); | |
function updatePlayer(delta) {{ | |
if(moveLeft) velocity.x -= acceleration * delta; | |
if(moveRight) velocity.x += acceleration * delta; | |
if(moveUp) velocity.y -= acceleration * delta; | |
if(moveDown) velocity.y += acceleration * delta; | |
velocity.x -= velocity.x * friction * delta; | |
velocity.y -= velocity.y * friction * delta; | |
xPos += velocity.x * delta; | |
zPos += velocity.y * delta; | |
xPos = Math.max(-40, Math.min(40, xPos)); | |
zPos = Math.max(-40, Math.min(40, zPos)); | |
playerMesh.position.set(xPos, 1, zPos); | |
ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`); | |
if(collect) {{ | |
for(let i = treasures.length - 1; i >= 0; i--) {{ | |
if(playerMesh.position.distanceTo(treasures[i].position) < 2) {{ | |
const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]); | |
scene.remove(treasures[i]); | |
treasures.splice(i, 1); | |
delete treasureMeshes[id]; | |
score += 50; | |
treasureCount += 1; | |
ws.send(`${{playerName}}|SCORE:${{score}}`); | |
ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`); | |
}} | |
}} | |
}} | |
camera.position.lerp(new THREE.Vector3(xPos, 50, zPos+50), 0.1); | |
camera.lookAt(new THREE.Vector3(xPos, 0, zPos)); | |
document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, (60 - (performance.now()/1000 - lastActive)).toFixed(0))}}s`; | |
document.getElementById('score').textContent = `Score: $${{score}}`; | |
document.getElementById('treasures').textContent = `Treasures: $${{treasureCount}}`; | |
}} | |
function updatePlayers(playerData) {{ | |
playerData.forEach(player => {{ | |
if(!playerMeshes[player.username]) {{ | |
const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2); | |
const material = new THREE.MeshPhongMaterial({{ color: player.color }}); | |
const mesh = new THREE.Mesh(geometry, material); | |
mesh.castShadow = true; | |
scene.add(mesh); | |
playerMeshes[player.username] = mesh; | |
}} | |
const mesh = playerMeshes[player.username]; | |
mesh.position.set(player.x, 1, player.z); | |
if(player.username === playerName) {{ | |
xPos = player.x; | |
zPos = player.z; | |
score = player.score; | |
treasureCount = player.treasures; | |
playerMesh.position.set(xPos, 1, zPos); | |
}} | |
}}); | |
document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`; | |
const leaderboard = playerData.sort((a, b) => b.score - a.score) | |
.map(p => `${{p.username}}: $${{p.score}}`) | |
.join('<br>'); | |
document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`; | |
}} | |
function updateTreasures(treasureData) {{ | |
treasureData.forEach(t => {{ | |
if(!treasureMeshes[t.id]) {{ | |
const treasure = new THREE.Mesh( | |
new THREE.SphereGeometry(1, 8, 8), | |
new THREE.MeshPhongMaterial({{ color: 0xffff00 }}) | |
); | |
treasure.position.set(t.x, 1, t.z); | |
treasure.castShadow = true; | |
treasureMeshes[t.id] = treasure; | |
treasures.push(treasure); | |
scene.add(treasure); | |
}} else {{ | |
treasureMeshes[t.id].position.set(t.x, 1, t.z); | |
}} | |
}}); | |
Object.keys(treasureMeshes).forEach(id => {{ | |
if(!treasureData.some(t => t.id === id)) {{ | |
scene.remove(treasureMeshes[id]); | |
treasures = treasures.filter(t => t !== treasureMeshes[id]); | |
delete treasureMeshes[id]; | |
}} | |
}}); | |
}} | |
function updateWorldObjects(objectData) {{ | |
objectData.forEach(obj => {{ | |
if(!worldObjectMeshes[obj.type + obj.x + obj.z]) {{ | |
const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2); | |
const material = new THREE.MeshPhongMaterial({{ color: obj.color }}); | |
const objMesh = new THREE.Mesh(geometry, material); | |
objMesh.position.set(obj.x, 1, obj.z); | |
objMesh.castShadow = true; | |
worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh; | |
worldObjects.push(objMesh); | |
scene.add(objMesh); | |
}} | |
}}); | |
}} | |
ws.onmessage = function(event) {{ | |
const data = event.data; | |
if(data.startsWith('MAP_UPDATE:')) {{ | |
const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]); | |
updatePlayers(playerData); | |
}} else if(data.startsWith('CHAT_UPDATE:')) {{ | |
const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]); | |
const chatBox = document.getElementById('chatBox'); | |
chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join(''); | |
chatBox.scrollTop = chatBox.scrollHeight; | |
}} else if(data.startsWith('GAME_STATE:')) {{ | |
const gameState = JSON.parse(data.split('GAME_STATE:')[1]); | |
updatePlayers(gameState.players); | |
updateTreasures(gameState.treasures); | |
updateWorldObjects(gameState.world_objects); | |
}} else if(!data.startsWith('PRAIRIE_UPDATE:')) {{ | |
const [sender, message] = data.split('|'); | |
const chatBox = document.getElementById('chatBox'); | |
chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`; | |
chatBox.scrollTop = chatBox.scrollHeight; | |
}} | |
}}; | |
let lastTime = performance.now(); | |
function animate() {{ | |
requestAnimationFrame(animate); | |
const currentTime = performance.now(); | |
const delta = (currentTime - lastTime) / 1000; | |
lastTime = currentTime; | |
updatePlayer(delta); | |
treasures.forEach(t => t.rotation.y += delta); | |
worldObjects.forEach(o => o.rotation.y += delta * 0.5); | |
renderer.render(scene, camera); | |
}} | |
animate(); | |
</script> | |
</body> | |
</html> | |
""" | |
# ----------------------------- | |
# Main Application | |
# ----------------------------- | |
def main(): | |
st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", 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"<div style='font-size: 24px;'>{obj['emoji']}</div>") | |
).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() | |