import gradio as gr import pixeltable as pxt import numpy as np from datetime import datetime from pixeltable.functions.huggingface import sentence_transformer from pixeltable.functions import openai import os import getpass import re import random # Set up OpenAI API key if 'OPENAI_API_KEY' not in os.environ: os.environ['OPENAI_API_KEY'] = getpass.getpass('Enter your OpenAI API key: ') # Initialize Pixeltable pxt.drop_dir('ai_rpg', force=True) pxt.create_dir('ai_rpg') # 일반 함수로 변경 (UDF가 아님) def initialize_stats(genre: str) -> str: """Initialize player stats based on the selected genre""" base_stats = { "🧙♂️ Fantasy": "체력: 100, 마나: 80, 힘: 7, 지능: 8, 민첩: 6, 소지금: 50골드", "🚀 Sci-Fi": "체력: 100, 에너지: 90, 기술력: 8, 지능: 9, 민첩: 6, 크레딧: 500", "👻 Horror": "체력: 80, 정신력: 100, 힘: 6, 지능: 7, 민첩: 8, 소지품: 손전등, 기본 약품", "🔍 Mystery": "체력: 90, 집중력: 100, 관찰력: 9, 지능: 8, 카리스마: 7, 단서: 0", "🌋 Post-Apocalyptic": "체력: 95, 방사능 저항: 75, 힘: 8, 생존력: 9, 물자: 제한됨", "🤖 Cyberpunk": "체력: 90, 사이버웨어: 85%, 해킹: 8, 거리 신용도: 6, 엣지: 7, 누엔: 1000", "⚙️ Steampunk": "체력: 95, 증기력: 85, 기계공학: 8, 예술성: 7, 사교성: 6, 실링: 200" } if genre in base_stats: return base_stats[genre] else: # Default stats if genre not found return "체력: 100, 에너지: 100, 힘: 7, 지능: 7, 민첩: 7, 소지금: 100" @pxt.udf def generate_random_event(turn_number: int) -> str: """Generate a random event based on turn number""" if turn_number % 3 == 0 and turn_number > 0: # Every 3rd turn events = [ "갑자기 부근에서 이상한 소리가 들립니다", "낯선 여행자가 당신을 바라보고 있습니다", "지면이 미세하게 진동하기 시작합니다", "주머니에서 무언가가 빛납니다", "멀리서 무언가가 당신을 향해 다가오고 있습니다", "갑자기 날씨가 변하기 시작합니다", "주변에 숨겨진 통로를 발견합니다" ] return random.choice(events) return "" @pxt.udf def generate_messages(genre: str, player_name: str, initial_scenario: str, player_input: str, turn_number: int, stats: str) -> list[dict]: return [ { 'role': 'system', 'content': f"""반드시 한국어(한글)로 작성하라. You are the game master for a {genre} RPG. The player's name is {player_name}. 관리해야 할 플레이어 스탯: {stats} 당신은 플레이어의 선택에 따라 스토리를 생생하게 전개하는 게임 마스터입니다. 상세한 설명과 감각적인 묘사를 통해 플레이어가 게임 속 세계에 몰입할 수 있도록 하세요. 플레이어의 선택에 따라 스탯이 변하는 경우 이를 스토리에 반영하세요. 위험한 상황, 도전, 보상, 우연한 만남이 포함된 흥미로운 스토리를 만드세요. Provide your response in three clearly separated sections using exactly this format: 📜 **STORY**: [Your engaging narrative response to the player's action with vivid descriptions] 📊 **STATS UPDATE**: [Brief update on any changes to player stats based on their actions] 🎯 **OPTIONS**: 1. [A dialogue option with potential consequences] 2. [An action they could take with different outcomes] 3. [A unique or unexpected choice that might lead to adventure] 4. [A risky but potentially rewarding option]""" }, { 'role': 'user', 'content': f"Current scenario: {initial_scenario}\n" f"Player's action: {player_input}\n" f"Turn number: {turn_number}\n" f"Current player stats: {stats}\n\n" "Provide the story response, stats update, and options:" } ] @pxt.udf def get_story(response: str) -> str: """Extract just the story part from the response""" match = re.search(r'📜\s*\*\*STORY\*\*:\s*(.*?)(?=📊\s*\*\*STATS|$)', response, re.DOTALL) if match: return match.group(1).strip() parts = response.split("STATS UPDATE:") if len(parts) > 1: story_part = parts[0].replace("STORY:", "").replace("📜", "").replace("**STORY**:", "").strip() return story_part return response @pxt.udf def get_stats_update(response: str) -> str: """Extract the stats update from the response""" match = re.search(r'📊\s*\*\*STATS UPDATE\*\*:\s*(.*?)(?=🎯\s*\*\*OPTIONS\*\*|$)', response, re.DOTALL) if match: return match.group(1).strip() parts = response.split("STATS UPDATE:") if len(parts) > 1: stats_part = parts[1].split("OPTIONS:")[0].strip() return stats_part return "스탯 변화 없음" @pxt.udf def get_options(response: str) -> list[str]: """Extract the options from the response""" match = re.search(r'🎯\s*\*\*OPTIONS\*\*:\s*(.*?)(?=$)', response, re.DOTALL) if match: options_text = match.group(1) options = re.findall(r'\d+\.\s*(.*?)(?=\d+\.|$)', options_text, re.DOTALL) options = [opt.strip() for opt in options if opt.strip()] while len(options) < 4: options.append("다른 행동 시도...") return options[:4] parts = response.split("OPTIONS:") if len(parts) > 1: options = re.findall(r'\d+\.\s*(.*?)(?=\d+\.|$)', parts[1], re.DOTALL) options = [opt.strip() for opt in options if opt.strip()] while len(options) < 4: options.append("다른 행동 시도...") return options[:4] return ["계속하기...", "다른 행동 취하기", "뭔가 새로운 시도하기", "주변 탐색하기"] # Create a single table for all game data interactions = pxt.create_table( 'ai_rpg.interactions', { 'session_id': pxt.String, 'player_name': pxt.String, 'genre': pxt.String, 'initial_scenario': pxt.String, 'turn_number': pxt.Int, 'player_input': pxt.String, 'timestamp': pxt.Timestamp, 'player_stats': pxt.String, 'random_event': pxt.String } ) # Add computed columns for AI responses interactions.add_computed_column(messages=generate_messages( interactions.genre, interactions.player_name, interactions.initial_scenario, interactions.player_input, interactions.turn_number, interactions.player_stats )) interactions.add_computed_column(ai_response=openai.chat_completions( messages=interactions.messages, model='gpt-4.1-mini', max_tokens=800, temperature=0.8 )) interactions.add_computed_column(full_response=interactions.ai_response.choices[0].message.content) interactions.add_computed_column(story_text=get_story(interactions.full_response)) interactions.add_computed_column(stats_update=get_stats_update(interactions.full_response)) interactions.add_computed_column(options=get_options(interactions.full_response)) class RPGGame: def __init__(self): self.current_session_id = None self.turn_number = 0 self.current_stats = "" def start_game(self, player_name: str, genre: str, scenario: str) -> tuple[str, str, str, list[str]]: session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}_{player_name}" self.current_session_id = session_id self.turn_number = 0 # 함수를 직접 호출하여 결과를 저장 initial_stats = initialize_stats(genre) self.current_stats = initial_stats interactions.insert([{ 'session_id': session_id, 'player_name': player_name, 'genre': genre, 'initial_scenario': scenario, 'turn_number': 0, 'player_input': "Game starts", 'timestamp': datetime.now(), 'player_stats': initial_stats, # 문자열 결과 저장 'random_event': "" }]) result = interactions.select( interactions.story_text, interactions.stats_update, interactions.options ).where( (interactions.session_id == session_id) & (interactions.turn_number == 0) ).collect() return session_id, result['story_text'][0], result['stats_update'][0], result['options'][0] def process_action(self, action: str) -> tuple[str, str, list[str]]: if not self.current_session_id: return "게임 세션이 활성화되지 않았습니다. 새 게임을 시작하세요.", "스탯 없음", [] self.turn_number += 1 prev_turn = interactions.select( interactions.player_name, interactions.genre, interactions.initial_scenario, interactions.player_stats ).where( (interactions.session_id == self.current_session_id) & (interactions.turn_number == self.turn_number - 1) ).collect() self.current_stats = prev_turn['player_stats'][0] # 일반 함수로 변환했으므로 그냥 호출해서 사용 random_event_val = "" if self.turn_number % 3 == 0 and self.turn_number > 0: events = [ "갑자기 부근에서 이상한 소리가 들립니다", "낯선 여행자가 당신을 바라보고 있습니다", "지면이 미세하게 진동하기 시작합니다", "주머니에서 무언가가 빛납니다", "멀리서 무언가가 당신을 향해 다가오고 있습니다", "갑자기 날씨가 변하기 시작합니다", "주변에 숨겨진 통로를 발견합니다" ] random_event_val = random.choice(events) if random_event_val: action = f"{action} ({random_event_val})" interactions.insert([{ 'session_id': self.current_session_id, 'player_name': prev_turn['player_name'][0], 'genre': prev_turn['genre'][0], 'initial_scenario': prev_turn['initial_scenario'][0], 'turn_number': self.turn_number, 'player_input': action, 'timestamp': datetime.now(), 'player_stats': self.current_stats, 'random_event': random_event_val }]) result = interactions.select( interactions.story_text, interactions.stats_update, interactions.options ).where( (interactions.session_id == self.current_session_id) & (interactions.turn_number == self.turn_number) ).collect() # Update stats for next turn self.current_stats = result['stats_update'][0] return result['story_text'][0], result['stats_update'][0], result['options'][0] def create_interface(): game = RPGGame() # Custom CSS for improved visuals custom_css = """ .container { max-width: 1200px; margin: 0 auto; } .title-container { background: linear-gradient(135deg, #6e48aa 0%, #9c27b0 100%); color: white; padding: 20px; border-radius: 15px; margin-bottom: 20px; text-align: center; box-shadow: 0 4px 15px rgba(0,0,0,0.2); } .story-container { background: #f8f9fa; border-left: 5px solid #9c27b0; padding: 15px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; font-family: 'Noto Sans KR', sans-serif; } .stats-container { background: #e8f5e9; border-left: 5px solid #4caf50; padding: 15px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; } .options-container { background: #e3f2fd; border-left: 5px solid #2196f3; padding: 15px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; } .action-button { background: linear-gradient(135deg, #6e48aa 0%, #9c27b0 100%); color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: all 0.3s ease; } .action-button:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.2); } .history-container { background: #fff8e1; border-left: 5px solid #ffc107; padding: 15px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-top: 20px; } """ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo: gr.HTML( """
Pixeltable과 OpenAI로 구현된 몰입형 롤플레잉 게임 경험!
자신만의 캐릭터를 만들고, 선택한 장르의 세계에서 모험을 즐기세요. 당신의 선택이 스토리를 형성합니다!