Cycle-Navigator / app.py
openfree's picture
Update app.py
300d6ac verified
raw
history blame
24.5 kB
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')
@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 ["๊ณ„์†ํ•˜๊ธฐ...", "๋‹ค๋ฅธ ํ–‰๋™ ์ทจํ•˜๊ธฐ", "๋ญ”๊ฐ€ ์ƒˆ๋กœ์šด ์‹œ๋„ํ•˜๊ธฐ", "์ฃผ๋ณ€ ํƒ์ƒ‰ํ•˜๊ธฐ"]
@pxt.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 ""
# 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
# ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋ฅผ ๋จผ์ € ๋ณ€์ˆ˜์— ์ €์žฅ
self.current_stats = initialize_stats(genre)
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': self.current_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 = generate_random_event(self.turn_number)
if random_event:
action = f"{action} ({random_event})"
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
}])
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(
"""
<div class="title-container">
<h1 style="margin-bottom: 0.5em; font-size: 2.5em;">๐ŸŽฒ AI RPG ์–ด๋“œ๋ฒค์ฒ˜</h1>
<p style="font-size: 1.2em;">Pixeltable๊ณผ OpenAI๋กœ ๊ตฌํ˜„๋œ ๋ชฐ์ž…ํ˜• ๋กคํ”Œ๋ ˆ์ž‰ ๊ฒŒ์ž„ ๊ฒฝํ—˜!</p>
</div>
"""
)
with gr.Row():
with gr.Column(scale=1):
with gr.Accordion("๐ŸŽฏ ์ด ์•ฑ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?", open=False):
gr.HTML(
"""
<div style="padding: 15px;">
<h3>AI RPG ์–ด๋“œ๋ฒค์ฒ˜๋Š” ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:</h3>
<ul style="list-style-type: none; padding-left: 5px;">
<li>๐ŸŽฎ <b>๋™์  ์Šคํ† ๋ฆฌํ…”๋ง:</b> AI๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ๋ชฐ์ž…ํ˜• ์ด์•ผ๊ธฐ ๊ฒฝํ—˜</li>
<li>๐Ÿ”„ <b>๊ฒŒ์ž„ ์ƒํƒœ ๊ด€๋ฆฌ:</b> Pixeltable๋กœ ๊ฒŒ์ž„ ์ƒํƒœ์™€ ๊ธฐ๋ก ์ถ”์ </li>
<li>๐Ÿ’ญ <b>์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์„ ํƒ์ง€:</b> ํ”Œ๋ ˆ์ด์–ด ํ–‰๋™์— ๋”ฐ๋ฅธ ๋งž์ถคํ˜• ์˜ต์…˜</li>
<li>๐Ÿค– <b>AI ์Šคํ† ๋ฆฌํ…”๋ง:</b> ์ƒ์ƒํ•œ ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ ์ƒ์„ฑ</li>
<li>๐Ÿ“Š <b>์บ๋ฆญํ„ฐ ์ƒํƒœ ์ถ”์ :</b> ๊ฒŒ์ž„ ์ง„ํ–‰์— ๋”ฐ๋ฅธ ์Šคํƒฏ ๋ณ€ํ™”</li>
</ul>
</div>
"""
)
with gr.Accordion("๐ŸŽจ ์บ๋ฆญํ„ฐ ์ƒ์„ฑ", open=True):
player_name = gr.Textbox(
label="๐Ÿ‘ค ์บ๋ฆญํ„ฐ ์ด๋ฆ„",
placeholder="๋‹น์‹ ์˜ ์บ๋ฆญํ„ฐ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”...",
container=False
)
genre = gr.Dropdown(
choices=[
"๐Ÿง™โ€โ™‚๏ธ Fantasy",
"๐Ÿš€ Sci-Fi",
"๐Ÿ‘ป Horror",
"๐Ÿ” Mystery",
"๐ŸŒ‹ Post-Apocalyptic",
"๐Ÿค– Cyberpunk",
"โš™๏ธ Steampunk"
],
label="๐ŸŽญ ์žฅ๋ฅด ์„ ํƒ",
container=False,
value="๐Ÿง™โ€โ™‚๏ธ Fantasy"
)
scenario = gr.Textbox(
label="๐Ÿ“– ์‹œ์ž‘ ์‹œ๋‚˜๋ฆฌ์˜ค",
lines=3,
placeholder="์ดˆ๊ธฐ ์„ค์ •๊ณผ ์ƒํ™ฉ์„ ์„ค๋ช…ํ•˜์„ธ์š”...",
container=False
)
start_button = gr.Button("๐ŸŽฎ ๋ชจํ—˜ ์‹œ์ž‘!", variant="primary")
with gr.Column(scale=2):
story_display = gr.Markdown(
label="๐Ÿ“œ ์Šคํ† ๋ฆฌ",
value="<div class='story-container'>๋ชจํ—˜์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์บ๋ฆญํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  '๋ชจํ—˜ ์‹œ์ž‘!' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”.</div>",
show_label=False
)
stats_display = gr.Markdown(
label="๐Ÿ“Š ์บ๋ฆญํ„ฐ ์Šคํƒฏ",
value="<div class='stats-container'>๋ชจํ—˜์„ ์‹œ์ž‘ํ•˜๋ฉด ์บ๋ฆญํ„ฐ ์Šคํƒฏ์ด ์ด๊ณณ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</div>",
show_label=False
)
gr.HTML("<div class='options-container'><h3>๐ŸŽฏ ๋‹ค์Œ ํ–‰๋™ ์„ ํƒ</h3></div>")
action_input = gr.Radio(
choices=[],
label="",
interactive=True
)
submit_action = gr.Button("โšก ํ–‰๋™ ์‹คํ–‰", variant="primary")
with gr.Row():
with gr.Column():
gr.HTML("<div class='history-container'><h3>๐Ÿ’ซ ์–ด๋“œ๋ฒค์ฒ˜ ์˜ˆ์‹œ</h3></div>")
gr.Examples(
examples=[
["์ด์ˆœ์‹ ", "๐Ÿง™โ€โ™‚๏ธ Fantasy", "๋‹น์‹ ์€ ์žŠํ˜€์ง„ ์‹ ๋น„ํ•œ ์ˆฒ์˜ ๊ฐ€์žฅ์ž๋ฆฌ์—์„œ ๋ˆˆ์„ ๋œน๋‹ˆ๋‹ค. ๋ฉ€๋ฆฌ์„œ ์„ฑ์˜ ์ฒจํƒ‘์ด ๋ณด์ด๊ณ , ๋‹น์‹ ์˜ ๋จธ๋ฆฌ์†์—๋Š” ์™•๊ตญ์„ ์œ„ํ˜‘ํ•˜๋Š” ๊ณ ๋Œ€ ๋งˆ๋ฒ•์— ๊ด€ํ•œ ๋‹จ์„œ๋งŒ์ด ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ‘์ž๊ธฐ ์ˆฒ์—์„œ ์ด์ƒํ•œ ๋น›์ด ๋ณด์ž…๋‹ˆ๋‹ค..."],
["๊น€์ง€์˜", "๐Ÿš€ Sci-Fi", "์šฐ์ฃผ์„  'ํ˜ธ๋ผ์ด์ฆŒ'์˜ ํ•ญํ•ด์‚ฌ๋กœ์„œ, ๋‹น์‹ ์€ ๋ฏธ์ง€์˜ ํ–‰์„ฑ ํƒ์‚ฌ ์ค‘ ๋น„์ƒ ์•Œ๋žŒ์— ๊นจ์–ด๋‚ฉ๋‹ˆ๋‹ค. ์„ ์žฅ๊ณผ ์—ฐ๋ฝ์ด ๋‘์ ˆ๋˜์—ˆ๊ณ , ์ƒ๋ช… ์œ ์ง€ ์‹œ์Šคํ…œ์ด ์ ์ฐจ ์‹คํŒจํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐ์šฉํ•œ ์„ ๋‚ด์—์„œ ์ด์ƒํ•œ ๋ฐœ๊ฑธ์Œ ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฝ๋‹ˆ๋‹ค..."],
["์ผ๋ก  ๋จธ์Šคํฌ", "๐Ÿค– Cyberpunk", "2077๋…„ ์„œ์šธ, ๋‹น์‹ ์€ ๋‰ด๋Ÿด๋งํฌ ์ธ๋”์ŠคํŠธ๋ฆฌ์˜ CEO์ž…๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์ตœ์‹  ๋‡Œ-์ปดํ“จํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ์ˆ ์ด ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋Šฅ๋ ฅ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค - ๊ทธ๋“ค์˜ ์ง‘์•ˆ ์‹๋ฌผ๊ณผ ๊ต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ํˆฌ์ž์ž ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ค€๋น„ํ•˜๋Š” ๋™์•ˆ, AI ๋น„์„œ๊ฐ€ ํ…Œ์ŠคํŠธ ์ฐธ๊ฐ€์ž๋“ค์ด ๋ถˆ๊ฐ€์‚ฌ์˜ํ•œ '์‹๋ฌผ ํ˜๋ช…'์„ ์กฐ์งํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๋ณด๊ณ ๋ฅผ ์ „ํ•ฉ๋‹ˆ๋‹ค..."],
["๊ณ ๋“  ๋žจ์ง€", "๐ŸŒ‹ Post-Apocalyptic", "๋‹น์‹ ์€ ๋‰ด ์„œ์šธ์˜ ๋งˆ์ง€๋ง‰ ๋งˆ์Šคํ„ฐ ์…ฐํ”„๋กœ, ์˜› ๋Ÿญ์…”๋ฆฌ ํ˜ธํ…” ํํ—ˆ์—์„œ ์ง€ํ•˜ ๋ ˆ์Šคํ† ๋ž‘์„ ์šด์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์š”๋ฆฌ๋Š” ์œ„ํ—˜ํ•œ ๋ฐฉ์‚ฌ๋Šฅ ๊ตฌ์—ญ์—์„œ๋งŒ ์ž๋ผ๋Š” ํฌ๊ท€ ๋ฒ„์„ฏ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋Š˜ ๋ฐค์˜ ๋น„๋ฐ€ ๋ชจ์ž„์„ ์ค€๋น„ํ•˜๋˜ ์ค‘, ์ •์ฐฐ๋ณ‘์ด ์ฃผ๋ณ€ ์ง€์—ญ์˜ ๊ฒฝ์Ÿ ์š”๋ฆฌ์‚ฌ ๊ฐฑ๋‹จ์— ๊ด€ํ•œ ๋ถˆ๊ธธํ•œ ์†Œ์‹์„ ๊ฐ€์ง€๊ณ  ๋Œ์•„์˜ต๋‹ˆ๋‹ค..."],
["์œ ์ •ํ˜ธ", "๐Ÿ‘ป Horror", "๋‹น์‹ ์€ ์นœ๊ตฌ์˜ ์ดˆ๋Œ€๋กœ ์‚ผ๋ฆผ ์† ์™ธ๋”ด ๋ณ„์žฅ์— ์ฃผ๋ง์„ ๋ณด๋‚ด๋Ÿฌ ์™”์Šต๋‹ˆ๋‹ค. ์ฒซ๋‚  ๋ฐค, ์ฐฝ๋ฐ–์œผ๋กœ ๋ณด์ด๋Š” ๊ธฐ์ดํ•œ ๋น›์— ์ด๋Œ๋ ค ์ˆฒ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋Œ์•„์˜ค๋Š” ๊ธธ์„ ์ฐพ์œผ๋ ค ํ•˜์ง€๋งŒ, ๋ณ„์žฅ์ด ๋ณด์ด์ง€ ์•Š๊ณ  ๋‚ฏ์„  ์•ˆ๊ฐœ๊ฐ€ ์ ์  ์ง™์–ด์ง‘๋‹ˆ๋‹ค. ๋ฉ€๋ฆฌ์„œ ๋ˆ„๊ตฐ๊ฐ€โ€”์•„๋‹ˆ, ๋ฌด์–ธ๊ฐ€๊ฐ€ ๋‹น์‹ ์„ ๋ถ€๋ฅด๋Š” ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฝ๋‹ˆ๋‹ค..."],
["๋ฐ•์ง€ํ›ˆ", "๐Ÿ” Mystery", "์‹ ์ž„ ํ˜•์‚ฌ๋กœ์„œ ๋‹น์‹ ์˜ ์ฒซ ์‚ฌ๊ฑด์€ ๋„์‹œ ์ตœ๊ณ ์˜ ๊ธฐ์ˆ  ๊ธฐ์—… CEO์˜ ์˜๋ฌธ์˜ ์‹ค์ข…์ž…๋‹ˆ๋‹ค. ๊ทธ์˜ ์‚ฌ๋ฌด์‹ค์—๋Š” ํ˜ˆํ”์ด ์—†๊ณ , ์œ ์ผํ•œ ๋‹จ์„œ๋Š” ์ฑ…์ƒ ์œ„์— ๋†“์ธ ์•”ํ˜ธํ™”๋œ ๋ฉ”๋ชจ์™€ ๊บผ์ ธ์žˆ๋Š” ๊ทธ์˜ ์ตœ์ฒจ๋‹จ AI ๋น„์„œ๋ฟ์ž…๋‹ˆ๋‹ค. ์กฐ์‚ฌ๋ฅผ ์‹œ์ž‘ํ•˜์ž๋งˆ์ž, ๋‹น์‹ ์€ CEO๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž‘์—…ํ•˜๋˜ ๋น„๋ฐ€ ํ”„๋กœ์ ํŠธ์— ๊ด€ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋“ฃ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค..."],
["์ด๋ฏผ์ˆ˜", "โš™๏ธ Steampunk", "์ฆ๊ธฐ์™€ ๊ธฐ์–ด๋กœ ๊ฐ€๋“ํ•œ ๋‰ด ์กฐ์„ ์—์„œ, ๋‹น์‹ ์€ ํ˜์‹ ์ ์ธ ๋น„ํ–‰์„  ์„ค๊ณ„์ž์ž…๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์ตœ์‹  ๋ฐœ๋ช…ํ’ˆ ์‹œ์—ฐ ์ค‘, ์ •๋ถ€์˜ ๋น„๋ฐ€ ์š”์›์ด ์ ‘๊ทผํ•ด ์œ„ํ—˜์— ์ฒ˜ํ•œ ํ™ฉ์‹ค ๊ฐ€์กฑ์„ ์œ„ํ•œ ๋น„๋ฐ€ ์ž„๋ฌด๋ฅผ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. ์ง€ํ•˜ ๋‹จ์ฒด๋“ค์ด ์™•์ขŒ๋ฅผ ์œ„ํ˜‘ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋‹น์‹ ์˜ ๋ฐœ๋ช…ํ’ˆ์ด ์™•๊ฐ€์˜ ์œ ์ผํ•œ ํฌ๋ง์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค..."]
],
inputs=[player_name, genre, scenario]
)
with gr.Column():
history_df = gr.Dataframe(
headers=["๐Ÿ“… ํ„ด", "๐ŸŽฏ ํ”Œ๋ ˆ์ด์–ด ํ–‰๋™", "๐Ÿ’ฌ ๊ฒŒ์ž„ ๋ฐ˜์‘"],
label="๐Ÿ“š ๋ชจํ—˜ ์—ญ์‚ฌ",
wrap=True,
row_count=5,
col_count=(3, "fixed")
)
def start_new_game(name, genre_choice, scenario_text):
if not name or not genre_choice or not scenario_text:
return (
"<div class='story-container'>๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ์‹œ์ž‘ํ•˜์„ธ์š”.</div>",
"<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
[],
[]
)
try:
_, initial_story, initial_stats, initial_options = game.start_game(name, genre_choice, scenario_text)
history_df = interactions.select(
turn=interactions.turn_number,
action=interactions.player_input,
response=interactions.story_text
).where(
interactions.session_id == game.current_session_id
).order_by(
interactions.turn_number
).collect().to_pandas()
history_data = [
[str(row['turn']), row['action'], row['response']]
for _, row in history_df.iterrows()
]
story_html = f"<div class='story-container'>{initial_story}</div>"
stats_html = f"<div class='stats-container'><h3>๐Ÿ“Š ์บ๋ฆญํ„ฐ ์ƒํƒœ</h3>{initial_stats}</div>"
return story_html, stats_html, gr.Radio(choices=initial_options, interactive=True), history_data
except Exception as e:
return (
f"<div class='story-container'>๊ฒŒ์ž„ ์‹œ์ž‘ ์˜ค๋ฅ˜: {str(e)}</div>",
"<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
[],
[]
)
def process_player_action(action_choice):
try:
if not action_choice:
return (
"<div class='story-container'>๊ณ„์†ํ•˜๋ ค๋ฉด ํ–‰๋™์„ ์„ ํƒํ•˜์„ธ์š”.</div>",
"<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
[],
[]
)
story, stats, options = game.process_action(action_choice)
history_df = interactions.select(
turn=interactions.turn_number,
action=interactions.player_input,
response=interactions.story_text
).where(
interactions.session_id == game.current_session_id
).order_by(
interactions.turn_number
).collect().to_pandas()
history_data = [
[str(row['turn']), row['action'], row['response']]
for _, row in history_df.iterrows()
]
story_html = f"<div class='story-container'>{story}</div>"
stats_html = f"<div class='stats-container'><h3>๐Ÿ“Š ์บ๋ฆญํ„ฐ ์ƒํƒœ</h3>{stats}</div>"
return story_html, stats_html, gr.Radio(choices=options, interactive=True), history_data
except Exception as e:
return (
f"<div class='story-container'>์˜ค๋ฅ˜: {str(e)}</div>",
"<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
[],
[]
)
start_button.click(
start_new_game,
inputs=[player_name, genre, scenario],
outputs=[story_display, stats_display, action_input, history_df]
)
submit_action.click(
process_player_action,
inputs=[action_input],
outputs=[story_display, stats_display, action_input, history_df]
)
gr.HTML("""
<div style="text-align: center; margin-top: 30px; padding: 20px; background: #f5f5f5; border-radius: 10px;">
<h3>๐ŸŒŸ AI RPG ์–ด๋“œ๋ฒค์ฒ˜ - Pixeltable๋กœ ์ œ์ž‘๋œ ๋ชฐ์ž…ํ˜• ๋กคํ”Œ๋ ˆ์ž‰ ๊ฒฝํ—˜</h3>
<p>์ž์‹ ๋งŒ์˜ ์บ๋ฆญํ„ฐ๋ฅผ ๋งŒ๋“ค๊ณ , ์„ ํƒํ•œ ์žฅ๋ฅด์˜ ์„ธ๊ณ„์—์„œ ๋ชจํ—˜์„ ์ฆ๊ธฐ์„ธ์š”. ๋‹น์‹ ์˜ ์„ ํƒ์ด ์Šคํ† ๋ฆฌ๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค!</p>
</div>
""")
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch()