Spaces:
Sleeping
Sleeping
import streamlit as st | |
import os | |
import random | |
import time | |
from PIL import Image | |
import json | |
from datetime import datetime | |
from pathlib import Path | |
import base64 | |
from io import BytesIO | |
# Fantasy name generator | |
def generate_fantasy_name(): | |
prefixes = ['Aer', 'Bal', 'Cal', 'Dor', 'El', 'Fae', 'Gor', 'Hel', 'Il', 'Jor', | |
'Kal', 'Lyr', 'Mel', 'Nym', 'Oro', 'Pyr', 'Qar', 'Ryn', 'Syl', 'Tyr'] | |
suffixes = ['ian', 'or', 'ion', 'us', 'ix', 'ar', 'en', 'yr', 'el', 'an', | |
'is', 'ax', 'on', 'ir', 'ex', 'az', 'er', 'eth', 'ys', 'ix'] | |
return random.choice(prefixes) + random.choice(suffixes) | |
# Initialize session state | |
if 'initialized' not in st.session_state: | |
st.session_state.initialized = False | |
st.session_state.game_state = { | |
'players': {}, | |
'chat_messages': [], | |
'tile_map': [], | |
'last_sync': time.time() | |
} | |
st.session_state.player_name = None | |
st.session_state.character_stats = None | |
# Character Stats Generation | |
def roll_stats(): | |
return { | |
'STR': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'DEX': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'CON': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'INT': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'WIS': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'CHA': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]), | |
'HP': random.randint(1, 20) * 2 + random.randint(1, 20), | |
'MAX_HP': 40, | |
'score': 0, | |
'created_at': time.time() | |
} | |
def save_character_sheet(player_name, stats): | |
"""Save character sheet as markdown""" | |
filepath = f"characters/{player_name}.md" | |
os.makedirs('characters', exist_ok=True) | |
markdown = f"""# {player_name}'s Character Sheet | |
## Stats | |
- **STR**: {stats['STR']} | |
- **DEX**: {stats['DEX']} | |
- **CON**: {stats['CON']} | |
- **INT**: {stats['INT']} | |
- **WIS**: {stats['WIS']} | |
- **CHA**: {stats['CHA']} | |
## Health | |
HP: {stats['HP']}/{stats['MAX_HP']} | |
## Score | |
Current Score: {stats['score']} | |
## Session Info | |
Created: {datetime.fromtimestamp(stats['created_at']).strftime('%Y-%m-%d %H:%M:%S')} | |
Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | |
""" | |
with open(filepath, 'w') as f: | |
f.write(markdown) | |
def load_character_sheet(player_name): | |
"""Load character sheet if it exists""" | |
filepath = f"characters/{player_name}.md" | |
if os.path.exists(filepath): | |
with open(filepath, 'r') as f: | |
return f.read() | |
return None | |
def create_game_js(): | |
"""Create JavaScript for real-time game updates""" | |
return """ | |
<script> | |
const gameState = { | |
playerName: null, | |
position: { x: 0, y: 0 }, | |
velocity: { x: 0, y: 0 }, | |
lastUpdate: Date.now(), | |
needsSync: false | |
}; | |
function updateClock() { | |
const now = new Date(); | |
document.getElementById('game-clock').textContent = | |
now.toLocaleTimeString(); | |
} | |
function handleMovement(e) { | |
const speed = 5; | |
switch(e.key.toLowerCase()) { | |
case 'w': case 'arrowup': | |
gameState.velocity.y = -speed; | |
break; | |
case 's': case 'arrowdown': | |
gameState.velocity.y = speed; | |
break; | |
case 'a': case 'arrowleft': | |
gameState.velocity.x = -speed; | |
break; | |
case 'd': case 'arrowright': | |
gameState.velocity.x = speed; | |
break; | |
case 'x': | |
gameState.velocity = { x: 0, y: 0 }; | |
break; | |
} | |
gameState.needsSync = true; | |
} | |
function updatePosition() { | |
const now = Date.now(); | |
const delta = (now - gameState.lastUpdate) / 1000; | |
gameState.position.x += gameState.velocity.x * delta; | |
gameState.position.y += gameState.velocity.y * delta; | |
// Boundary checks | |
gameState.position.x = Math.max(0, Math.min(gameState.position.x, 19)); | |
gameState.position.y = Math.max(0, Math.min(gameState.position.y, 14)); | |
gameState.lastUpdate = now; | |
// Update player marker position | |
const marker = document.querySelector('.player-marker'); | |
if (marker) { | |
marker.style.left = `${gameState.position.x * 60 + 25}px`; | |
marker.style.top = `${gameState.position.y * 60 + 25}px`; | |
} | |
} | |
function syncWithServer() { | |
if (gameState.needsSync) { | |
// Send position to Streamlit | |
window.parent.postMessage({ | |
type: 'streamlit:sync', | |
position: gameState.position | |
}, '*'); | |
gameState.needsSync = false; | |
} | |
} | |
// Initialize game loop | |
setInterval(updatePosition, 16); // ~60 FPS | |
setInterval(syncWithServer, 5000); // Sync every 5 seconds | |
setInterval(updateClock, 1000); // Update clock every second | |
// Set up event listeners | |
document.addEventListener('keydown', handleMovement); | |
</script> | |
""" | |
def main(): | |
# Sidebar for player info and controls | |
st.sidebar.title("Player Info") | |
# Player name handling | |
if st.session_state.player_name is None: | |
default_name = generate_fantasy_name() | |
player_name = st.sidebar.text_input("Enter your name or use generated name:", | |
value=default_name) | |
if st.sidebar.button("Start Playing"): | |
st.session_state.player_name = player_name | |
if st.session_state.character_stats is None: | |
st.session_state.character_stats = roll_stats() | |
save_character_sheet(player_name, st.session_state.character_stats) | |
st.rerun() | |
else: | |
# Show current name and allow changes | |
new_name = st.sidebar.text_input("Your name:", | |
value=st.session_state.player_name) | |
if new_name != st.session_state.player_name: | |
old_name = st.session_state.player_name | |
st.session_state.player_name = new_name | |
# Rename character sheet | |
os.rename(f"characters/{old_name}.md", | |
f"characters/{new_name}.md") | |
st.rerun() | |
# Display character sheet | |
character_sheet = load_character_sheet(st.session_state.player_name) | |
if character_sheet: | |
st.sidebar.markdown(character_sheet) | |
# Movement controls with emojis | |
st.sidebar.markdown("### Movement Controls") | |
move_cols = st.sidebar.columns(3) | |
move_cols[1].button("⬆️", key="up") | |
cols = st.sidebar.columns(3) | |
cols[0].button("⬅️", key="left") | |
cols[1].button("⬇️", key="down") | |
cols[2].button("➡️", key="right") | |
st.sidebar.button("🛑", key="stop") | |
# Main game area | |
st.title("Multiplayer Tile Game") | |
# Game clock | |
st.markdown('<div id="game-clock" style="font-size: 24px;"></div>', | |
unsafe_allow_html=True) | |
# Game board and JavaScript | |
if st.session_state.player_name: | |
st.components.v1.html(create_game_js(), height=600) | |
if __name__ == "__main__": | |
main() |