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 import numpy as np # Constants GRID_WIDTH = 16 GRID_HEIGHT = 9 REFRESH_RATE = 5 INTERACTION_RADIUS = 2 POINTS_PER_INTERACTION = 1 # Singleton cache for game resources @st.experimental_singleton def get_game_images(): """Cache game images to improve performance""" return { 'tile': Image.new('RGB', (50, 50), color='green'), 'player': Image.new('RGB', (50, 50), color='blue'), 'other_player': Image.new('RGB', (50, 50), color='red') } @st.experimental_singleton def get_name_components(): """Cache name generation components""" return { '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'] } def generate_fantasy_name(): """Generate random fantasy name using cached components""" name_parts = get_name_components() return random.choice(name_parts['prefixes']) + random.choice(name_parts['suffixes']) def load_game_state(): """Load game state from query parameters""" if 'player_name' in st.query_params: st.session_state.player_name = st.query_params.player_name st.session_state.position = { 'x': int(st.query_params.get('x', random.randint(0, GRID_WIDTH - 1))), 'y': int(st.query_params.get('y', random.randint(0, GRID_HEIGHT - 1))) } st.session_state.character_stats = { 'STR': int(st.query_params.get('STR', 10)), 'DEX': int(st.query_params.get('DEX', 10)), 'CON': int(st.query_params.get('CON', 10)), 'INT': int(st.query_params.get('INT', 10)), 'WIS': int(st.query_params.get('WIS', 10)), 'CHA': int(st.query_params.get('CHA', 10)), 'HP': int(st.query_params.get('HP', 20)), 'MAX_HP': int(st.query_params.get('MAX_HP', 40)), 'score': int(st.query_params.get('score', 0)), 'created_at': float(st.query_params.get('created_at', time.time())) } def save_game_state(): """Save game state to query parameters""" if st.session_state.player_name and st.session_state.character_stats: params = { 'player_name': st.session_state.player_name, 'x': str(st.session_state.position['x']), 'y': str(st.session_state.position['y']), **{k: str(v) for k, v in st.session_state.character_stats.items()} } st.query_params.from_dict(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 = [] load_game_state() @st.experimental_singleton(validate=lambda x: time.time() - x['last_update'] < 60) def load_all_players(): """Cache and load all player states with validation""" 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) players[player_data['name']] = player_data return {'players': players, 'last_update': time.time()} 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) save_game_state() 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() def update_nearby_players(): """Update list of nearby players and calculate score gains""" all_players = load_all_players()['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() st.session_state.nearby_players = nearby def create_game_board(): """Create and display the game board""" all_players = load_all_players()['players'] images = get_game_images() # 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(images['player'], use_column_width=True) cols[x].markdown(f"**You** ({st.session_state.character_stats['score']} pts)") elif player_here: cols[x].image(images['other_player'], use_column_width=True) cols[x].markdown(f"**{player_here}** ({all_players[player_here]['stats']['score']} pts)") else: cols[x].image(images['tile'], 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 = { '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() } save_player_state() 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() # Clear game state button if st.sidebar.button("Clear Game State"): st.query_params.clear() st.experimental_singleton.clear() st.rerun() # 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()