Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from datetime import datetime, timedelta | |
| import hashlib | |
| import uuid | |
| import time | |
| from config import APP_TITLE, APP_DESCRIPTION, GAME_CONFIG | |
| from database.nocodb import nocodb_client | |
| from llm.together import together_client | |
| # Page Configuration | |
| st.set_page_config( | |
| page_title=APP_TITLE, | |
| page_icon="⚔️", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS Styles | |
| st.markdown(""" | |
| <style> | |
| .main { | |
| background-color: #f0f2f6; | |
| } | |
| .stButton>button { | |
| background-color: #4CAF50; | |
| color: white; | |
| border-radius: 5px; | |
| padding: 10px 20px; | |
| font-weight: bold; | |
| transition: all 0.3s ease; | |
| } | |
| .stButton>button:hover { | |
| background-color: #45a049; | |
| transform: translateY(-2px); | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| } | |
| .character-card { | |
| background-color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .character-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.2); | |
| } | |
| .battle-log { | |
| background-color: #fff3cd; | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin: 10px 0; | |
| border-left: 5px solid #ffc107; | |
| } | |
| .success-message { | |
| color: #28a745; | |
| font-weight: bold; | |
| padding: 10px; | |
| border-radius: 5px; | |
| background-color: #d4edda; | |
| } | |
| .error-message { | |
| color: #dc3545; | |
| font-weight: bold; | |
| padding: 10px; | |
| border-radius: 5px; | |
| background-color: #f8d7da; | |
| } | |
| .sidebar-content { | |
| padding: 20px; | |
| background-color: #f8f9fa; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .tab-content { | |
| padding: 20px; | |
| background-color: white; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| } | |
| .stat-value { | |
| font-size: 1.2em; | |
| font-weight: bold; | |
| color: #2c3e50; | |
| } | |
| .class-badge { | |
| padding: 5px 10px; | |
| border-radius: 15px; | |
| font-size: 0.9em; | |
| font-weight: bold; | |
| } | |
| .warrior { background-color: #e74c3c; color: white; } | |
| .mage { background-color: #3498db; color: white; } | |
| .rogue { background-color: #2ecc71; color: white; } | |
| .priest { background-color: #9b59b6; color: white; } | |
| .archer { background-color: #f1c40f; color: white; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize Session State | |
| if 'user_id' not in st.session_state: | |
| st.session_state.user_id = None | |
| if 'character' not in st.session_state: | |
| st.session_state.character = None | |
| if 'characters' not in st.session_state: | |
| st.session_state.characters = [] | |
| if 'is_logged_in' not in st.session_state: | |
| st.session_state.is_logged_in = False | |
| if 'current_tab' not in st.session_state: | |
| st.session_state.current_tab = "Character Info" | |
| def hash_password(password: str) -> str: | |
| """Hash password using SHA256""" | |
| return hashlib.sha256(password.encode()).hexdigest() | |
| def login_user(username: str, password: str) -> bool: | |
| """User login""" | |
| try: | |
| users = nocodb_client.get_records("users", { | |
| "where": f"(username,eq,{username})" | |
| }) | |
| if users and users[0]["password"] == hash_password(password): | |
| st.session_state.user_id = users[0]["Id"] | |
| st.session_state.is_logged_in = True | |
| # Load user's characters | |
| characters = nocodb_client.get_records("characters", { | |
| "where": f"(user_id,eq,{users[0]['Id']})" | |
| }) | |
| st.session_state.characters = characters | |
| return True | |
| except Exception as e: | |
| st.error(f"Login failed: {str(e)}") | |
| return False | |
| def register_user(username: str, password: str) -> bool: | |
| """User registration""" | |
| try: | |
| # Check if username exists | |
| existing_users = nocodb_client.get_records("users", { | |
| "where": f"(username,eq,{username})" | |
| }) | |
| if existing_users: | |
| st.error("Username already exists") | |
| return False | |
| # Create new user | |
| user = { | |
| "username": username, | |
| "password": hash_password(password), | |
| "created_time": datetime.now().isoformat() | |
| } | |
| result = nocodb_client.create_record("users", user) | |
| return True | |
| except Exception as e: | |
| st.error(f"Registration failed: {str(e)}") | |
| return False | |
| def load_character(character_id: str) -> None: | |
| """Load specified character""" | |
| try: | |
| characters = nocodb_client.get_records("characters", { | |
| "where": f"(Id,eq,{character_id})" | |
| }) | |
| if characters: | |
| # Ensure all required attributes exist | |
| character = characters[0] | |
| required_fields = { | |
| 'level': 1, | |
| 'EXP': 0, | |
| 'ATK': GAME_CONFIG["base_stats"]["ATK"], | |
| 'DEF': GAME_CONFIG["base_stats"]["DEF"], | |
| 'MAG': GAME_CONFIG["base_stats"]["MAG"], | |
| 'DEX': GAME_CONFIG["base_stats"]["DEX"], | |
| 'LUK': GAME_CONFIG["base_stats"]["LUK"] | |
| } | |
| # Use default values for missing fields | |
| for field, default_value in required_fields.items(): | |
| if field not in character: | |
| character[field] = default_value | |
| # Update database record | |
| nocodb_client.update_record("characters", character_id, {field: default_value}) | |
| st.session_state.character = character | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"Failed to load character: {str(e)}") | |
| def create_character(name: str, character_class: str) -> bool: | |
| """Create new character""" | |
| try: | |
| # Get class base attributes | |
| class_stats = GAME_CONFIG["class_base_stats"][character_class] | |
| # Ensure all required fields have default values | |
| character = { | |
| "user_id": int(st.session_state.user_id), | |
| "name": name, | |
| "class": character_class, | |
| "level": 1, | |
| "EXP": 0, | |
| "ATK": class_stats["ATK"], | |
| "DEF": class_stats["DEF"], | |
| "MAG": class_stats["MAG"], | |
| "DEX": class_stats["DEX"], | |
| "LUK": class_stats["LUK"], | |
| "created_time": datetime.now().isoformat(), | |
| "last_login": datetime.now().isoformat() | |
| } | |
| result = nocodb_client.create_record("characters", character) | |
| if result: | |
| # Ensure returned result contains all required fields | |
| for field, value in character.items(): | |
| if field not in result: | |
| result[field] = value | |
| st.session_state.character = result | |
| return True | |
| return False | |
| except Exception as e: | |
| st.error(f"Failed to create character: {str(e)}") | |
| return False | |
| def update_character_stats(character_id: str, stats: dict) -> bool: | |
| """Update character attributes""" | |
| try: | |
| nocodb_client.update_record("characters", character_id, stats) | |
| return True | |
| except Exception as e: | |
| st.error(f"Failed to update attributes: {str(e)}") | |
| return False | |
| def get_class_badge(character_class: str) -> str: | |
| """Get class badge style""" | |
| class_styles = { | |
| "Warrior": "warrior", | |
| "Mage": "mage", | |
| "Rogue": "rogue", | |
| "Priest": "priest", | |
| "Archer": "archer" | |
| } | |
| return f'<span class="class-badge {class_styles.get(character_class, "")}">{character_class}</span>' | |
| def create_character_stats_chart(character): | |
| """Create character attributes radar chart""" | |
| stats = { | |
| 'Attributes': ['Attack', 'Defense', 'Magic', 'Dexterity', 'Luck'], | |
| 'Values': [ | |
| character['ATK'], | |
| character['DEF'], | |
| character['MAG'], | |
| character['DEX'], | |
| character['LUK'] | |
| ] | |
| } | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatterpolar( | |
| r=stats['Values'], | |
| theta=stats['Attributes'], | |
| fill='toself', | |
| name='Current Stats' | |
| )) | |
| # Add class baseline | |
| class_stats = GAME_CONFIG["class_base_stats"][character['class']] | |
| base_stats = [ | |
| class_stats['ATK'], | |
| class_stats['DEF'], | |
| class_stats['MAG'], | |
| class_stats['DEX'], | |
| class_stats['LUK'] | |
| ] | |
| fig.add_trace(go.Scatterpolar( | |
| r=base_stats, | |
| theta=stats['Attributes'], | |
| fill='toself', | |
| name='Class Baseline' | |
| )) | |
| fig.update_layout( | |
| polar=dict( | |
| radialaxis=dict( | |
| visible=True, | |
| range=[0, max(max(stats['Values']), max(base_stats)) * 1.2] | |
| )), | |
| showlegend=True, | |
| title='Character Attributes Radar Chart' | |
| ) | |
| return fig | |
| def create_level_progress_chart(character): | |
| """Create level progress gauge""" | |
| # Calculate required experience (example: 1000 exp per level) | |
| exp_needed = character['level'] * 1000 | |
| progress = (character['EXP'] / exp_needed) * 100 | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=progress, | |
| title={'text': f"Level Progress (Lv.{character['level']})"}, | |
| gauge={ | |
| 'axis': {'range': [0, 100]}, | |
| 'bar': {'color': "darkblue"}, | |
| 'steps': [ | |
| {'range': [0, 100], 'color': "lightgray"} | |
| ], | |
| 'threshold': { | |
| 'line': {'color': "red", 'width': 4}, | |
| 'thickness': 0.75, | |
| 'value': 100 | |
| } | |
| } | |
| )) | |
| fig.update_layout( | |
| height=200, | |
| margin=dict(l=20, r=20, t=50, b=20) | |
| ) | |
| return fig | |
| def create_friend_ranking_table(character, available_opponents): | |
| """Create friend ranking table""" | |
| try: | |
| # Ensure current character has all attributes | |
| current_char = { | |
| 'name': character['name'], | |
| 'level': character.get('level', 1), | |
| 'ATK': character.get('ATK', 10), | |
| 'DEF': character.get('DEF', 10), | |
| 'MAG': character.get('MAG', 10), | |
| 'DEX': character.get('DEX', 10), | |
| 'LUK': character.get('LUK', 10), | |
| 'class': character['class'], | |
| 'username': 'You' | |
| } | |
| current_char['total_stats'] = current_char['ATK'] + current_char['DEF'] + current_char['MAG'] + current_char['DEX'] + current_char['LUK'] | |
| # Ensure all opponents have attributes | |
| for opp in available_opponents: | |
| opp['ATK'] = opp.get('ATK', 10) | |
| opp['DEF'] = opp.get('DEF', 10) | |
| opp['MAG'] = opp.get('MAG', 10) | |
| opp['DEX'] = opp.get('DEX', 10) | |
| opp['LUK'] = opp.get('LUK', 10) | |
| opp['level'] = opp.get('level', 1) | |
| opp['total_stats'] = opp['ATK'] + opp['DEF'] + opp['MAG'] + opp['DEX'] + opp['LUK'] | |
| all_chars = available_opponents + [current_char] | |
| # Sort by total stats | |
| sorted_chars = sorted(all_chars, key=lambda x: x['total_stats'], reverse=True) | |
| # Create table data | |
| table_data = [] | |
| for i, char in enumerate(sorted_chars, 1): | |
| table_data.append({ | |
| "Rank": i, | |
| "Character": char['name'], | |
| "Class": char['class'], | |
| "Level": char['level'], | |
| "Attack": char['ATK'], | |
| "Defense": char['DEF'], | |
| "Magic": char['MAG'], | |
| "Dexterity": char['DEX'], | |
| "Luck": char['LUK'], | |
| "Total Stats": char['total_stats'], | |
| "Player": char['username'] | |
| }) | |
| # Create DataFrame | |
| df = pd.DataFrame(table_data) | |
| return df | |
| except Exception as e: | |
| st.error(f"Failed to create ranking table: {str(e)}") | |
| return None | |
| # Add rate limit decorator | |
| def rate_limit(max_requests=10, time_window=60): | |
| """Rate limit decorator""" | |
| last_reset = time.time() | |
| request_count = 0 | |
| def decorator(func): | |
| def wrapper(*args, **kwargs): | |
| nonlocal last_reset, request_count | |
| current_time = time.time() | |
| # Reset counter | |
| if current_time - last_reset >= time_window: | |
| last_reset = current_time | |
| request_count = 0 | |
| # Check limit | |
| if request_count >= max_requests: | |
| time.sleep(time_window - (current_time - last_reset)) | |
| last_reset = time.time() | |
| request_count = 0 | |
| request_count += 1 | |
| return func(*args, **kwargs) | |
| return wrapper | |
| return decorator | |
| # Add retry mechanism | |
| def retry_on_error(max_retries=3, delay=1): | |
| """Retry decorator""" | |
| def decorator(func): | |
| def wrapper(*args, **kwargs): | |
| for attempt in range(max_retries): | |
| try: | |
| return func(*args, **kwargs) | |
| except Exception as e: | |
| if "429" in str(e): | |
| if attempt < max_retries - 1: | |
| time.sleep(delay * (attempt + 1)) | |
| continue | |
| raise e | |
| return func(*args, **kwargs) | |
| return wrapper | |
| return decorator | |
| # Modify get available opponents function | |
| def get_available_opponents(current_user_id): | |
| """Get available opponents list""" | |
| try: | |
| # Use cache mechanism to get all users and character data | |
| users = nocodb_client.get_records("users") | |
| if not users: | |
| return [] | |
| # Get all character data (one-time fetch) | |
| all_characters = nocodb_client.get_records("characters") | |
| # Filter and process data in memory | |
| available_opponents = [] | |
| for user in users: | |
| if user["Id"] != current_user_id: # Exclude self | |
| # Filter user's characters in memory | |
| user_characters = [char for char in all_characters if char.get("user_id") == user["Id"]] | |
| if user_characters: | |
| for char in user_characters: | |
| available_opponents.append({ | |
| "id": char["Id"], | |
| "name": char["name"], | |
| "class": char["class"], | |
| "level": char.get("level", 1), | |
| "username": user["username"], | |
| "ATK": char.get("ATK", 10), | |
| "DEF": char.get("DEF", 10), | |
| "MAG": char.get("MAG", 10), | |
| "DEX": char.get("DEX", 10), | |
| "LUK": char.get("LUK", 10) | |
| }) | |
| return available_opponents | |
| except Exception as e: | |
| st.error(f"Failed to get friend list: {str(e)}") | |
| return [] | |
| # Modify battle record retrieval logic | |
| def get_battle_records(character_id): | |
| """Get battle records (with cache)""" | |
| try: | |
| # Add error handling and retry logic | |
| max_retries = 3 | |
| retry_delay = 2 | |
| for attempt in range(max_retries): | |
| try: | |
| battles = nocodb_client.get_records("battles", { | |
| "where": f"(attacker,eq,{character_id})" | |
| }) | |
| return battles[-5:] if battles else [] # Return last 5 battles | |
| except Exception as e: | |
| if "429" in str(e) and attempt < max_retries - 1: | |
| time.sleep(retry_delay * (attempt + 1)) | |
| continue | |
| raise e | |
| except Exception as e: | |
| st.error(f"Failed to get battle records: {str(e)}") | |
| return [] | |
| # Modify Boss challenge record retrieval logic | |
| def get_boss_challenge_records(character_id): | |
| """Get Boss challenge records (with cache)""" | |
| try: | |
| # Add error handling and retry logic | |
| max_retries = 3 | |
| retry_delay = 2 | |
| for attempt in range(max_retries): | |
| try: | |
| challenges = nocodb_client.get_records("boss_challenges", { | |
| "where": f"(team,like,{character_id})" | |
| }) | |
| return challenges[-5:] if challenges else [] # Return last 5 challenges | |
| except Exception as e: | |
| if "429" in str(e) and attempt < max_retries - 1: | |
| time.sleep(retry_delay * (attempt + 1)) | |
| continue | |
| raise e | |
| except Exception as e: | |
| st.error(f"Failed to get challenge records: {str(e)}") | |
| return [] | |
| # Sidebar | |
| with st.sidebar: | |
| st.title("⚔️ RPG Adventure Game") | |
| if not st.session_state.is_logged_in: | |
| with st.container(): | |
| st.markdown('<div class="sidebar-content">', unsafe_allow_html=True) | |
| tab1, tab2 = st.tabs(["Login", "Register"]) | |
| with tab1: | |
| with st.form("login_form"): | |
| login_username = st.text_input("Username") | |
| login_password = st.text_input("Password", type="password") | |
| if st.form_submit_button("Login", use_container_width=True): | |
| if login_user(login_username, login_password): | |
| st.success("Login successful!") | |
| st.rerun() | |
| with tab2: | |
| with st.form("register_form"): | |
| reg_username = st.text_input("New Username") | |
| reg_password = st.text_input("New Password", type="password") | |
| reg_confirm = st.text_input("Confirm Password", type="password") | |
| if st.form_submit_button("Register", use_container_width=True): | |
| if reg_password == reg_confirm: | |
| if register_user(reg_username, reg_password): | |
| st.success("Registration successful! Please login") | |
| else: | |
| st.error("Passwords do not match") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| else: | |
| with st.container(): | |
| st.markdown('<div class="sidebar-content">', unsafe_allow_html=True) | |
| st.success(f"Welcome back!") | |
| # Character selection | |
| if st.session_state.characters: | |
| st.markdown("### Select Character") | |
| character_names = [f"{char['name']} ({char['class']})" for char in st.session_state.characters] | |
| selected_character = st.selectbox( | |
| "Select character to use", | |
| character_names, | |
| index=0 if not st.session_state.character else character_names.index(f"{st.session_state.character['name']} ({st.session_state.character['class']})") | |
| ) | |
| if selected_character: | |
| selected_id = st.session_state.characters[character_names.index(selected_character)]["Id"] | |
| if not st.session_state.character or st.session_state.character["Id"] != selected_id: | |
| load_character(selected_id) | |
| # Navigation | |
| st.markdown("### Navigation") | |
| tabs = ["Character Info", "Character Development", "PVP Battle", "Boss Challenge"] | |
| selected_tab = st.radio("Select function", tabs, index=tabs.index(st.session_state.current_tab)) | |
| st.session_state.current_tab = selected_tab | |
| if st.button("Logout", use_container_width=True): | |
| st.session_state.is_logged_in = False | |
| st.session_state.user_id = None | |
| st.session_state.character = None | |
| st.session_state.characters = [] | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Model selection | |
| with st.container(): | |
| st.markdown('<div class="sidebar-content">', unsafe_allow_html=True) | |
| st.markdown("### AI Model Settings") | |
| try: | |
| models = together_client.get_available_models() | |
| if models: | |
| model_names = [model["name"] for model in models] | |
| selected_model = st.selectbox( | |
| "Select AI model", | |
| model_names, | |
| index=0 | |
| ) | |
| together_client.set_model(selected_model) | |
| except Exception as e: | |
| st.warning(f"Failed to get model list: {str(e)}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Main interface | |
| if not st.session_state.is_logged_in: | |
| st.markdown(""" | |
| <div style='text-align: center; padding: 50px;'> | |
| <h1>Welcome to RPG Adventure Game</h1> | |
| <p>Please login or register to start the game</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| # Check NocoDB connection | |
| try: | |
| nocodb_client.get_records("characters") | |
| except Exception as e: | |
| st.error(f"Unable to connect to NocoDB database: {str(e)}") | |
| st.stop() | |
| # Display content based on selected tab | |
| if st.session_state.current_tab == "Character Info": | |
| if not st.session_state.character: | |
| st.markdown(""" | |
| <div style='text-align: center;'> | |
| <h2>Create Your Character</h2> | |
| <p>Choose a class and start your adventure!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with st.form("character_creation"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Character Name") | |
| character_class = st.selectbox( | |
| "Choose Class", | |
| GAME_CONFIG["classes"] | |
| ) | |
| with col2: | |
| st.markdown("### Class Description") | |
| class_descriptions = { | |
| "Warrior": "Excels in melee combat with high attack and defense", | |
| "Mage": "Masters magic with powerful magical attacks", | |
| "Rogue": "Agile and flexible, skilled in critical hits and evasion", | |
| "Priest": "Expert in healing and support magic", | |
| "Archer": "Ranged attack specialist with high accuracy" | |
| } | |
| st.write(class_descriptions[character_class]) | |
| if st.form_submit_button("Create Character", use_container_width=True): | |
| if name: | |
| if create_character(name, character_class): | |
| st.success("Character created successfully!") | |
| st.rerun() | |
| else: | |
| st.warning("Please enter character name") | |
| else: | |
| # Character basic info card | |
| st.markdown(f""" | |
| <div class='character-card'> | |
| <h2>{st.session_state.character['name']} {get_class_badge(st.session_state.character['class'])}</h2> | |
| <div style='display: flex; justify-content: space-between;'> | |
| <div> | |
| <p>ID:<span class='stat-value'>{st.session_state.character['Id']}</span></p> | |
| <p>Level:<span class='stat-value'>{st.session_state.character['level']}</span></p> | |
| <p>EXP:<span class='stat-value'>{st.session_state.character['EXP']}</span></p> | |
| </div> | |
| <div> | |
| <p>Attack:<span class='stat-value'>{st.session_state.character['ATK']}</span></p> | |
| <p>Defense:<span class='stat-value'>{st.session_state.character['DEF']}</span></p> | |
| <p>Magic:<span class='stat-value'>{st.session_state.character['MAG']}</span></p> | |
| <p>Dexterity:<span class='stat-value'>{st.session_state.character['DEX']}</span></p> | |
| <p>Luck:<span class='stat-value'>{st.session_state.character['LUK']}</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Add attribute radar chart | |
| st.plotly_chart(create_character_stats_chart(st.session_state.character), use_container_width=True) | |
| # Add level progress gauge | |
| st.plotly_chart(create_level_progress_chart(st.session_state.character), use_container_width=True) | |
| # Add class description | |
| st.markdown("### Class Features") | |
| class_descriptions = { | |
| "Warrior": "Excels in melee combat with high attack and defense, serving as the main damage dealer and tank in the team.", | |
| "Mage": "Masters magic with powerful magical attacks, but has weak defense and needs team protection.", | |
| "Rogue": "Agile and flexible, skilled in critical hits and evasion, serving as the team's striker.", | |
| "Priest": "Expert in healing and support magic, playing a crucial support role in the team.", | |
| "Archer": "Ranged attack specialist with high accuracy, suitable for kiting tactics." | |
| } | |
| st.info(class_descriptions[st.session_state.character['class']]) | |
| elif st.session_state.current_tab == "Character Development": | |
| if not st.session_state.character: | |
| st.warning("Please create a character first") | |
| else: | |
| st.markdown(""" | |
| <div style='text-align: center;'> | |
| <h2>Character Development</h2> | |
| <p>Enhance your character through daily login and attribute point allocation!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| try: | |
| # Check daily login reward | |
| last_login = st.session_state.character.get("last_login") | |
| if last_login: | |
| try: | |
| # Try to parse datetime string | |
| if isinstance(last_login, str): | |
| last_login_dt = datetime.fromisoformat(last_login) | |
| if datetime.now() - last_login_dt > timedelta(days=1): | |
| # Update level and experience | |
| new_level = st.session_state.character["level"] + 1 | |
| new_exp = st.session_state.character["EXP"] + 100 # Add 100 exp per level | |
| # Update character data | |
| update_data = { | |
| "level": new_level, | |
| "EXP": new_exp, | |
| "last_login": datetime.now().isoformat() | |
| } | |
| if update_character_stats(st.session_state.character["Id"], update_data): | |
| st.session_state.character["level"] = new_level | |
| st.session_state.character["EXP"] = new_exp | |
| st.session_state.character["last_login"] = datetime.now().isoformat() | |
| st.markdown(f""" | |
| <div class='success-message'> | |
| Daily login reward received: | |
| <br>Level up: {new_level-1} → {new_level} | |
| <br>Experience gained: +100 | |
| <br>Received 5 attribute points! | |
| </div> | |
| """, unsafe_allow_html=True) | |
| except (ValueError, TypeError): | |
| # Ignore date check if parsing fails | |
| pass | |
| # Attribute point allocation | |
| st.markdown("### Attribute Point Allocation") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| atk_points = st.number_input("Attack", min_value=0, value=0) | |
| def_points = st.number_input("Defense", min_value=0, value=0) | |
| mag_points = st.number_input("Magic", min_value=0, value=0) | |
| with col2: | |
| dex_points = st.number_input("Dexterity", min_value=0, value=0) | |
| luk_points = st.number_input("Luck", min_value=0, value=0) | |
| if st.button("Confirm Allocation", use_container_width=True): | |
| total_points = atk_points + def_points + mag_points + dex_points + luk_points | |
| if total_points <= GAME_CONFIG["max_daily_points"]: | |
| # Update character attributes | |
| st.session_state.character["ATK"] += atk_points | |
| st.session_state.character["DEF"] += def_points | |
| st.session_state.character["MAG"] += mag_points | |
| st.session_state.character["DEX"] += dex_points | |
| st.session_state.character["LUK"] += luk_points | |
| if update_character_stats(st.session_state.character["Id"], { | |
| "ATK": st.session_state.character["ATK"], | |
| "DEF": st.session_state.character["DEF"], | |
| "MAG": st.session_state.character["MAG"], | |
| "DEX": st.session_state.character["DEX"], | |
| "LUK": st.session_state.character["LUK"] | |
| }): | |
| st.success("Attribute points allocated successfully!") | |
| else: | |
| st.warning(f"Maximum {GAME_CONFIG['max_daily_points']} attribute points per day") | |
| except Exception as e: | |
| st.error(f"Character development system error: {str(e)}") | |
| elif st.session_state.current_tab == "PVP Battle": | |
| if not st.session_state.character: | |
| st.warning("Please create a character first") | |
| else: | |
| st.markdown(""" | |
| <div style='text-align: center;'> | |
| <h2>PVP Battle</h2> | |
| <p>Battle with other players and show your strength!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Add friend challenge option | |
| st.markdown("### Friend Challenge") | |
| try: | |
| # Use new opponent retrieval function | |
| available_opponents = get_available_opponents(st.session_state.user_id) | |
| if available_opponents: | |
| # Display rankings | |
| st.markdown("### Friend Rankings") | |
| ranking_table = create_friend_ranking_table(st.session_state.character, available_opponents) | |
| if ranking_table is not None: | |
| # Use Streamlit's table display | |
| st.dataframe( | |
| ranking_table, | |
| use_container_width=True, | |
| hide_index=True, | |
| column_config={ | |
| "Rank": st.column_config.NumberColumn("Rank", width="small"), | |
| "Character": st.column_config.TextColumn("Character", width="medium"), | |
| "Class": st.column_config.TextColumn("Class", width="small"), | |
| "Level": st.column_config.NumberColumn("Level", width="small"), | |
| "Attack": st.column_config.NumberColumn("Attack", width="small"), | |
| "Defense": st.column_config.NumberColumn("Defense", width="small"), | |
| "Magic": st.column_config.NumberColumn("Magic", width="small"), | |
| "Dexterity": st.column_config.NumberColumn("Dexterity", width="small"), | |
| "Luck": st.column_config.NumberColumn("Luck", width="small"), | |
| "Total Stats": st.column_config.NumberColumn("Total Stats", width="small"), | |
| "Player": st.column_config.TextColumn("Player", width="medium") | |
| } | |
| ) | |
| # Highlight current player's row | |
| current_player_row = ranking_table[ranking_table['Character'] == st.session_state.character['name']] | |
| if not current_player_row.empty: | |
| st.markdown(f""" | |
| <div style='background-color: #FFD700; padding: 10px; border-radius: 5px; margin-top: 10px;'> | |
| <strong>Your Rank: #{current_player_row['Rank'].iloc[0]}</strong> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Create selection box | |
| opponent_options = [f"{opp['name']} ({opp['class']}) - Level {opp['level']} - Player: {opp['username']}" for opp in available_opponents] | |
| selected_opponent = st.selectbox( | |
| "Select friend to challenge", | |
| opponent_options, | |
| index=0 | |
| ) | |
| if st.button("Challenge Friend", use_container_width=True): | |
| selected_id = available_opponents[opponent_options.index(selected_opponent)]["id"] | |
| try: | |
| # Get opponent information | |
| opponents = nocodb_client.get_records("characters", { | |
| "where": f"(Id,eq,{selected_id})" | |
| }) | |
| if opponents: | |
| opponent = opponents[0] | |
| # Generate battle story | |
| battle_story = together_client.generate_battle_story( | |
| st.session_state.character, | |
| opponent | |
| ) | |
| # Save battle record | |
| battle_record = { | |
| "attacker": st.session_state.character["Id"], | |
| "defender": opponent["Id"], | |
| "battle_story": battle_story, | |
| "created_time": datetime.now().isoformat() | |
| } | |
| nocodb_client.create_record("battles", battle_record) | |
| # Display battle story | |
| st.markdown("### Battle Story") | |
| st.markdown(f""" | |
| <div class='battle-log'> | |
| {battle_story} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| except Exception as e: | |
| st.error(f"Battle failed: {str(e)}") | |
| else: | |
| st.info("No friends available for challenge") | |
| except Exception as e: | |
| st.error(f"Failed to get friend list: {str(e)}") | |
| st.markdown("---") | |
| st.markdown("### Direct Challenge") | |
| opponent_id = st.text_input("Enter opponent's ID") | |
| if st.button("Start Battle", use_container_width=True): | |
| if opponent_id: | |
| try: | |
| # Get opponent information | |
| opponents = nocodb_client.get_records("characters", { | |
| "where": f"(Id,eq,{opponent_id})" | |
| }) | |
| if opponents: | |
| opponent = opponents[0] | |
| # Generate battle story | |
| battle_story = together_client.generate_battle_story( | |
| st.session_state.character, | |
| opponent | |
| ) | |
| # Save battle record | |
| battle_record = { | |
| "attacker": st.session_state.character["Id"], | |
| "defender": opponent["Id"], | |
| "battle_story": battle_story, | |
| "created_time": datetime.now().isoformat() | |
| } | |
| nocodb_client.create_record("battles", battle_record) | |
| # Display battle story | |
| st.markdown("### Battle Story") | |
| st.markdown(f""" | |
| <div class='battle-log'> | |
| {battle_story} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.warning("Opponent not found") | |
| except Exception as e: | |
| st.error(f"Battle failed: {str(e)}") | |
| else: | |
| st.warning("Please enter opponent's ID") | |
| with col2: | |
| st.markdown("### Recent Battle Records") | |
| try: | |
| battles = get_battle_records(st.session_state.character["Id"]) | |
| if battles: | |
| for battle in battles: # Show last 5 battles | |
| st.markdown(f""" | |
| <div class='battle-log'> | |
| <p>Battle Time: {battle['created_time']}</p> | |
| <p>Battle Story: {battle['battle_story'][:100]}...</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.info("No battle records") | |
| except Exception as e: | |
| st.error(f"Failed to get battle records: {str(e)}") | |
| elif st.session_state.current_tab == "Boss Challenge": | |
| if not st.session_state.character: | |
| st.warning("Please create a character first") | |
| else: | |
| st.markdown(""" | |
| <div style='text-align: center;'> | |
| <h2>Boss Challenge</h2> | |
| <p>Form a team and challenge powerful Bosses for great rewards!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Display current Boss information | |
| st.markdown("### Current Boss") | |
| for boss in GAME_CONFIG["bosses"]: | |
| st.markdown(f""" | |
| <div class='character-card'> | |
| <h3>{boss['name']}</h3> | |
| <p>Attack: <span class='stat-value'>{boss['ATK']}</span></p> | |
| <p>Defense: <span class='stat-value'>{boss['DEF']}</span></p> | |
| <p>Magic: <span class='stat-value'>{boss['MAG']}</span></p> | |
| <p>Dexterity: <span class='stat-value'>{boss['DEX']}</span></p> | |
| <p>Luck: <span class='stat-value'>{boss['LUK']}</span></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Team challenge | |
| team_member_ids = st.text_area("Enter teammate IDs (one per line)") | |
| if st.button("Start Challenge", use_container_width=True): | |
| if team_member_ids: | |
| try: | |
| # Get teammate information | |
| team = [st.session_state.character] | |
| for id in team_member_ids.strip().split("\n"): | |
| members = nocodb_client.get_records("characters", { | |
| "where": f"(Id,eq,{id})" | |
| }) | |
| if members: | |
| team.append(members[0]) | |
| if len(team) > 1: | |
| # Generate Boss battle story | |
| boss_story = together_client.generate_boss_story(team, GAME_CONFIG["bosses"][0]) | |
| # Save challenge record | |
| challenge_record = { | |
| "team": ",".join([str(member["Id"]) for member in team]), | |
| "boss_id": 1, | |
| "story": boss_story, | |
| "created_time": datetime.now().isoformat() | |
| } | |
| nocodb_client.create_record("boss_challenges", challenge_record) | |
| # Display challenge story | |
| st.markdown("### Challenge Story") | |
| st.markdown(f""" | |
| <div class='battle-log'> | |
| {boss_story} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.warning("Need at least one teammate") | |
| except Exception as e: | |
| st.error(f"Challenge failed: {str(e)}") | |
| else: | |
| st.warning("Please enter teammate IDs") | |
| with col2: | |
| st.markdown("### Recent Challenge Records") | |
| try: | |
| challenges = get_boss_challenge_records(st.session_state.character["Id"]) | |
| if challenges: | |
| for challenge in challenges: # Show last 5 challenges | |
| st.markdown(f""" | |
| <div class='battle-log'> | |
| <p>Challenge Time: {challenge['created_time']}</p> | |
| <p>Challenge Story: {challenge['story'][:100]}...</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.info("No challenge records") | |
| except Exception as e: | |
| st.error(f"Failed to get challenge records: {str(e)}") | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style='text-align: center;'> | |
| <p>Powered by Together AI, NocoDB, and Hugging Face Spaces</p> | |
| </div> | |
| """, unsafe_allow_html=True) |