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 from urllib.parse import urlencode import numpy as np # Constants for game board GRID_WIDTH = 16 GRID_HEIGHT = 9 REFRESH_RATE = 5 # seconds INTERACTION_RADIUS = 2 # tiles POINTS_PER_INTERACTION = 1 # 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) def get_query_params(): """Get game state from URL query parameters""" query_params = st.experimental_get_query_params() if 'player_name' in query_params: st.session_state.player_name = query_params['player_name'][0] st.session_state.position = { 'x': int(query_params.get('x', [random.randint(0, GRID_WIDTH - 1)])[0]), 'y': int(query_params.get('y', [random.randint(0, GRID_HEIGHT - 1)])[0]) } st.session_state.character_stats = { 'STR': int(query_params.get('STR', [10])[0]), 'DEX': int(query_params.get('DEX', [10])[0]), 'CON': int(query_params.get('CON', [10])[0]), 'INT': int(query_params.get('INT', [10])[0]), 'WIS': int(query_params.get('WIS', [10])[0]), 'CHA': int(query_params.get('CHA', [10])[0]), 'HP': int(query_params.get('HP', [20])[0]), 'MAX_HP': int(query_params.get('MAX_HP', [40])[0]), 'score': int(query_params.get('score', [0])[0]), 'created_at': float(query_params.get('created_at', [time.time()])[0]) } def update_query_params(): """Update URL query parameters with current game state""" if st.session_state.player_name and st.session_state.character_stats: params = { 'player_name': st.session_state.player_name, 'x': st.session_state.position['x'], 'y': st.session_state.position['y'], **st.session_state.character_stats } st.experimental_set_query_params(**params) # Initialize session state if 'initialized' not in st.session_state: st.session_state.initialized = False st.session_state.game_state = { 'players': {}, 'chat_messages': [], 'last_sync': time.time() } st.session_state.player_name = None st.session_state.character_stats = None st.session_state.position = { 'x': random.randint(0, GRID_WIDTH - 1), 'y': random.randint(0, GRID_HEIGHT - 1) } st.session_state.last_move = time.time() st.session_state.nearby_players = [] get_query_params() def save_player_state(): """Save player state to JSON file""" if st.session_state.player_name: player_data = { 'name': st.session_state.player_name, 'position': st.session_state.position, 'stats': st.session_state.character_stats, 'last_update': time.time() } os.makedirs('players', exist_ok=True) with open(f"players/{st.session_state.player_name}.json", 'w') as f: json.dump(player_data, f) def load_all_players(): """Load all player states from JSON files""" players = {} if os.path.exists('players'): for filename in os.listdir('players'): if filename.endswith('.json'): with open(f"players/{filename}", 'r') as f: player_data = json.load(f) # Only include active players (updated in last minute) if time.time() - player_data['last_update'] < 60: players[player_data['name']] = player_data return players def calculate_distance(pos1, pos2): """Calculate Manhattan distance between two positions with wrapping""" dx = min(abs(pos1['x'] - pos2['x']), GRID_WIDTH - abs(pos1['x'] - pos2['x'])) dy = min(abs(pos1['y'] - pos2['y']), GRID_HEIGHT - abs(pos1['y'] - pos2['y'])) return dx + dy def update_position(direction): """Update player position with wrapping""" if direction == "up": st.session_state.position['y'] = (st.session_state.position['y'] - 1) % GRID_HEIGHT elif direction == "down": st.session_state.position['y'] = (st.session_state.position['y'] + 1) % GRID_HEIGHT elif direction == "left": st.session_state.position['x'] = (st.session_state.position['x'] - 1) % GRID_WIDTH elif direction == "right": st.session_state.position['x'] = (st.session_state.position['x'] + 1) % GRID_WIDTH st.session_state.last_move = time.time() save_player_state() update_query_params() def update_nearby_players(): """Update list of nearby players and calculate score gains""" all_players = load_all_players() nearby = [] score_gain = 0 for player_name, player_data in all_players.items(): if player_name != st.session_state.player_name: distance = calculate_distance(st.session_state.position, player_data['position']) if distance <= INTERACTION_RADIUS: nearby.append({ 'name': player_name, 'distance': distance, 'score': player_data['stats']['score'] }) score_gain += POINTS_PER_INTERACTION if score_gain > 0: st.session_state.character_stats['score'] += score_gain save_player_state() update_query_params() st.session_state.nearby_players = nearby def create_game_board(): """Create and display the game board""" all_players = load_all_players() # Create placeholder images for tiles tile_image = Image.new('RGB', (50, 50), color='green') player_image = Image.new('RGB', (50, 50), color='blue') other_player_image = Image.new('RGB', (50, 50), color='red') # Create columns for each row for y in range(GRID_HEIGHT): cols = st.columns(GRID_WIDTH) for x in range(GRID_WIDTH): current_pos = {'x': x, 'y': y} # Check if any player is on this tile player_here = None for player_name, player_data in all_players.items(): if (player_data['position']['x'] == x and player_data['position']['y'] == y): player_here = player_name # Display appropriate image and tooltip if x == st.session_state.position['x'] and y == st.session_state.position['y']: cols[x].image(player_image, use_column_width=True) cols[x].markdown(f"**You** ({st.session_state.character_stats['score']} pts)") elif player_here: cols[x].image(other_player_image, use_column_width=True) cols[x].markdown(f"**{player_here}** ({all_players[player_here]['stats']['score']} pts)") else: cols[x].image(tile_image, use_column_width=True) def main(): # Update nearby players on refresh update_nearby_players() # 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_player_state() update_query_params() st.rerun() else: # Display nearby players st.sidebar.markdown("### Nearby Players") for player in st.session_state.nearby_players: st.sidebar.markdown( f"**{player['name']}** - {player['distance']} tiles away - {player['score']} pts" ) # Movement controls st.sidebar.markdown("### Movement Controls") move_cols = st.sidebar.columns(3) if move_cols[1].button("⬆️", key="up"): update_position("up") st.rerun() cols = st.sidebar.columns(3) if cols[0].button("⬅️", key="left"): update_position("left") st.rerun() if cols[1].button("⬇️", key="down"): update_position("down") st.rerun() if cols[2].button("➡️", key="right"): update_position("right") st.rerun() # Display bookmark URL st.sidebar.markdown("### Save Your Progress") st.sidebar.markdown( "Bookmark this URL to continue where you left off:" ) st.sidebar.code(st.experimental_get_query_params()) # Main game area st.title("Multiplayer Tile Game") # Display game board create_game_board() # Auto-refresh logic if (time.time() - st.session_state.last_move) > REFRESH_RATE: st.session_state.last_move = time.time() save_player_state() st.rerun() if __name__ == "__main__": main()