ColoradoMadLibs / backup2.app.py
awacke1's picture
Update backup2.app.py
564d6f6 verified
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
import glob
import pytz
from collections import defaultdict
import pandas as pd
# Patch asyncio for nesting
nest_asyncio.apply()
# Page Config
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 Config
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 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 "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)}
],
"world_objects": [],
"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."""
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():
"""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)}
],
"world_objects": [],
"history": []
}
return update_game_state(state)
# Agent Class for Players
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()
# Helpers
def format_timestamp(username=""):
now = datetime.now(pytz.timezone('US/Central')).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)
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
# 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 = 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)
# 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({"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 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(),
'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"]))
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 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:
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) # $1 per second
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 EDGE_TTS_VOICES if c["name"] == username)
md_file, audio_file = await save_chat_entry(username, result, voice, True)
return md_file, 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['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 Game HTML
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>Rocky Mountain Quest 3D</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 players = {{}};
const playerMeshes = {{}};
let treasures = [];
const treasureMeshes = {{}};
let worldObjects = [];
const worldObjectMeshes = {{}};
let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
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: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
playerMesh.position.set(xPos, 1, zPos);
playerMesh.castShadow = true;
scene.add(playerMesh);
players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
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);
}}
}});
}}
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) {{
const speed = 20;
if (moveLeft && xPos > -40) xPos -= speed * delta;
if (moveRight && xPos < 40) xPos += speed * delta;
if (moveUp && zPos > -40) zPos -= speed * delta;
if (moveDown && zPos < 40) zPos += speed * delta;
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.set(xPos, 50, zPos + 50);
camera.lookAt(xPos, 0, zPos);
const timeout = 60 - (performance.now() / 1000 - lastActive);
document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.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);
players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
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}}`;
}}
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 Game Loop
def main():
# Top Center Chat and Character Display
st.markdown(f"<h2 style='text-align: center;'>Welcome, {st.session_state.username}!</h2>", unsafe_allow_html=True)
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)
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.session_state.last_activity = time.time()
st.success(f"๐ŸŒ„ +10 points! New Score: ${st.session_state.score}")
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()
# Demo Buttons
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)
chat_content = asyncio.run(load_chat())
st.text_area("๐Ÿ“œ Quest Log", "\n".join(chat_content[-10:]), height=200, disabled=True)
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
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.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)
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.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()