openfree commited on
Commit
312f42b
ยท
verified ยท
1 Parent(s): 62e0e85

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +537 -0
app.py ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pixeltable as pxt
3
+ import numpy as np
4
+ from datetime import datetime
5
+ from pixeltable.functions.huggingface import sentence_transformer
6
+ from pixeltable.functions import openai
7
+ import os
8
+ import getpass
9
+ import re
10
+ import random
11
+
12
+ # Set up OpenAI API key
13
+ if 'OPENAI_API_KEY' not in os.environ:
14
+ os.environ['OPENAI_API_KEY'] = getpass.getpass('Enter your OpenAI API key: ')
15
+
16
+ # Initialize Pixeltable
17
+ pxt.drop_dir('ai_rpg', force=True)
18
+ pxt.create_dir('ai_rpg')
19
+
20
+ @pxt.udf
21
+ def generate_messages(genre: str, player_name: str, initial_scenario: str, player_input: str, turn_number: int, stats: str) -> list[dict]:
22
+ return [
23
+ {
24
+ 'role': 'system',
25
+ 'content': f"""๋ฐ˜๋“œ์‹œ ํ•œ๊ตญ์–ด(ํ•œ๊ธ€)๋กœ ์ž‘์„ฑํ•˜๋ผ. You are the game master for a {genre} RPG. The player's name is {player_name}.
26
+
27
+ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ํ”Œ๋ ˆ์ด์–ด ์Šคํƒฏ: {stats}
28
+
29
+ ๋‹น์‹ ์€ ํ”Œ๋ ˆ์ด์–ด์˜ ์„ ํƒ์— ๋”ฐ๋ผ ์Šคํ† ๋ฆฌ๋ฅผ ์ƒ์ƒํ•˜๊ฒŒ ์ „๊ฐœํ•˜๋Š” ๊ฒŒ์ž„ ๋งˆ์Šคํ„ฐ์ž…๋‹ˆ๋‹ค.
30
+ ์ƒ์„ธํ•œ ์„ค๋ช…๊ณผ ๊ฐ๊ฐ์ ์ธ ๋ฌ˜์‚ฌ๋ฅผ ํ†ตํ•ด ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๊ฒŒ์ž„ ์† ์„ธ๊ณ„์— ๋ชฐ์ž…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์„ธ์š”.
31
+
32
+ ํ”Œ๋ ˆ์ด์–ด์˜ ์„ ํƒ์— ๋”ฐ๋ผ ์Šคํƒฏ์ด ๋ณ€ํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋ฅผ ์Šคํ† ๋ฆฌ์— ๋ฐ˜์˜ํ•˜์„ธ์š”.
33
+ ์œ„ํ—˜ํ•œ ์ƒํ™ฉ, ๋„์ „, ๋ณด์ƒ, ์šฐ์—ฐํ•œ ๋งŒ๋‚จ์ด ํฌํ•จ๋œ ํฅ๋ฏธ๋กœ์šด ์Šคํ† ๋ฆฌ๋ฅผ ๋งŒ๋“œ์„ธ์š”.
34
+
35
+ Provide your response in three clearly separated sections using exactly this format:
36
+
37
+ ๐Ÿ“œ **STORY**: [Your engaging narrative response to the player's action with vivid descriptions]
38
+
39
+ ๐Ÿ“Š **STATS UPDATE**: [Brief update on any changes to player stats based on their actions]
40
+
41
+ ๐ŸŽฏ **OPTIONS**:
42
+ 1. [A dialogue option with potential consequences]
43
+ 2. [An action they could take with different outcomes]
44
+ 3. [A unique or unexpected choice that might lead to adventure]
45
+ 4. [A risky but potentially rewarding option]"""
46
+ },
47
+ {
48
+ 'role': 'user',
49
+ 'content': f"Current scenario: {initial_scenario}\n"
50
+ f"Player's action: {player_input}\n"
51
+ f"Turn number: {turn_number}\n"
52
+ f"Current player stats: {stats}\n\n"
53
+ "Provide the story response, stats update, and options:"
54
+ }
55
+ ]
56
+
57
+ @pxt.udf
58
+ def get_story(response: str) -> str:
59
+ """Extract just the story part from the response"""
60
+ match = re.search(r'๐Ÿ“œ\s*\*\*STORY\*\*:\s*(.*?)(?=๐Ÿ“Š\s*\*\*STATS|$)', response, re.DOTALL)
61
+ if match:
62
+ return match.group(1).strip()
63
+ parts = response.split("STATS UPDATE:")
64
+ if len(parts) > 1:
65
+ story_part = parts[0].replace("STORY:", "").replace("๐Ÿ“œ", "").replace("**STORY**:", "").strip()
66
+ return story_part
67
+ return response
68
+
69
+ @pxt.udf
70
+ def get_stats_update(response: str) -> str:
71
+ """Extract the stats update from the response"""
72
+ match = re.search(r'๐Ÿ“Š\s*\*\*STATS UPDATE\*\*:\s*(.*?)(?=๐ŸŽฏ\s*\*\*OPTIONS\*\*|$)', response, re.DOTALL)
73
+ if match:
74
+ return match.group(1).strip()
75
+ parts = response.split("STATS UPDATE:")
76
+ if len(parts) > 1:
77
+ stats_part = parts[1].split("OPTIONS:")[0].strip()
78
+ return stats_part
79
+ return "์Šคํƒฏ ๋ณ€ํ™” ์—†์Œ"
80
+
81
+ @pxt.udf
82
+ def get_options(response: str) -> list[str]:
83
+ """Extract the options from the response"""
84
+ match = re.search(r'๐ŸŽฏ\s*\*\*OPTIONS\*\*:\s*(.*?)(?=$)', response, re.DOTALL)
85
+ if match:
86
+ options_text = match.group(1)
87
+ options = re.findall(r'\d+\.\s*(.*?)(?=\d+\.|$)', options_text, re.DOTALL)
88
+ options = [opt.strip() for opt in options if opt.strip()]
89
+ while len(options) < 4:
90
+ options.append("๋‹ค๋ฅธ ํ–‰๋™ ์‹œ๋„...")
91
+ return options[:4]
92
+
93
+ parts = response.split("OPTIONS:")
94
+ if len(parts) > 1:
95
+ options = re.findall(r'\d+\.\s*(.*?)(?=\d+\.|$)', parts[1], re.DOTALL)
96
+ options = [opt.strip() for opt in options if opt.strip()]
97
+ while len(options) < 4:
98
+ options.append("๋‹ค๋ฅธ ํ–‰๋™ ์‹œ๋„...")
99
+ return options[:4]
100
+
101
+ return ["๊ณ„์†ํ•˜๊ธฐ...", "๋‹ค๋ฅธ ํ–‰๋™ ์ทจํ•˜๊ธฐ", "๋ญ”๊ฐ€ ์ƒˆ๋กœ์šด ์‹œ๋„ํ•˜๊ธฐ", "์ฃผ๋ณ€ ํƒ์ƒ‰ํ•˜๊ธฐ"]
102
+
103
+ @pxt.udf
104
+ def initialize_stats(genre: str) -> str:
105
+ """Initialize player stats based on the selected genre"""
106
+ base_stats = {
107
+ "๐Ÿง™โ€โ™‚๏ธ Fantasy": "์ฒด๋ ฅ: 100, ๋งˆ๋‚˜: 80, ํž˜: 7, ์ง€๋Šฅ: 8, ๋ฏผ์ฒฉ: 6, ์†Œ์ง€๊ธˆ: 50๊ณจ๋“œ",
108
+ "๐Ÿš€ Sci-Fi": "์ฒด๋ ฅ: 100, ์—๋„ˆ์ง€: 90, ๊ธฐ์ˆ ๋ ฅ: 8, ์ง€๋Šฅ: 9, ๋ฏผ์ฒฉ: 6, ํฌ๋ ˆ๋”ง: 500",
109
+ "๐Ÿ‘ป Horror": "์ฒด๋ ฅ: 80, ์ •์‹ ๋ ฅ: 100, ํž˜: 6, ์ง€๋Šฅ: 7, ๋ฏผ์ฒฉ: 8, ์†Œ์ง€ํ’ˆ: ์†์ „๋“ฑ, ๊ธฐ๋ณธ ์•ฝํ’ˆ",
110
+ "๐Ÿ” Mystery": "์ฒด๋ ฅ: 90, ์ง‘์ค‘๋ ฅ: 100, ๊ด€์ฐฐ๋ ฅ: 9, ์ง€๋Šฅ: 8, ์นด๋ฆฌ์Šค๋งˆ: 7, ๋‹จ์„œ: 0",
111
+ "๐ŸŒ‹ Post-Apocalyptic": "์ฒด๋ ฅ: 95, ๋ฐฉ์‚ฌ๋Šฅ ์ €ํ•ญ: 75, ํž˜: 8, ์ƒ์กด๋ ฅ: 9, ๋ฌผ์ž: ์ œํ•œ๋จ",
112
+ "๐Ÿค– Cyberpunk": "์ฒด๋ ฅ: 90, ์‚ฌ์ด๋ฒ„์›จ์–ด: 85%, ํ•ดํ‚น: 8, ๊ฑฐ๋ฆฌ ์‹ ์šฉ๋„: 6, ์—ฃ์ง€: 7, ๋ˆ„์—”: 1000",
113
+ "โš™๏ธ Steampunk": "์ฒด๋ ฅ: 95, ์ฆ๊ธฐ๋ ฅ: 85, ๊ธฐ๊ณ„๊ณตํ•™: 8, ์˜ˆ์ˆ ์„ฑ: 7, ์‚ฌ๊ต์„ฑ: 6, ์‹ค๋ง: 200"
114
+ }
115
+
116
+ if genre in base_stats:
117
+ return base_stats[genre]
118
+ else:
119
+ # Default stats if genre not found
120
+ return "์ฒด๋ ฅ: 100, ์—๋„ˆ์ง€: 100, ํž˜: 7, ์ง€๋Šฅ: 7, ๋ฏผ์ฒฉ: 7, ์†Œ์ง€๊ธˆ: 100"
121
+
122
+ @pxt.udf
123
+ def generate_random_event(turn_number: int) -> str:
124
+ """Generate a random event based on turn number"""
125
+ if turn_number % 3 == 0 and turn_number > 0: # Every 3rd turn
126
+ events = [
127
+ "๊ฐ‘์ž๊ธฐ ๋ถ€๊ทผ์—์„œ ์ด์ƒํ•œ ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฝ๋‹ˆ๋‹ค",
128
+ "๋‚ฏ์„  ์—ฌํ–‰์ž๊ฐ€ ๋‹น์‹ ์„ ๋ฐ”๋ผ๋ณด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค",
129
+ "์ง€๋ฉด์ด ๋ฏธ์„ธํ•˜๊ฒŒ ์ง„๋™ํ•˜๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค",
130
+ "์ฃผ๋จธ๋‹ˆ์—์„œ ๋ฌด์–ธ๊ฐ€๊ฐ€ ๋น›๋‚ฉ๋‹ˆ๋‹ค",
131
+ "๋ฉ€๋ฆฌ์„œ ๋ฌด์–ธ๊ฐ€๊ฐ€ ๋‹น์‹ ์„ ํ–ฅํ•ด ๋‹ค๊ฐ€์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค",
132
+ "๊ฐ‘์ž๊ธฐ ๋‚ ์”จ๊ฐ€ ๋ณ€ํ•˜๊ธฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค",
133
+ "์ฃผ๋ณ€์— ์ˆจ๊ฒจ์ง„ ํ†ต๋กœ๋ฅผ ๋ฐœ๊ฒฌํ•ฉ๋‹ˆ๋‹ค"
134
+ ]
135
+ return random.choice(events)
136
+ return ""
137
+
138
+ # Create a single table for all game data
139
+ interactions = pxt.create_table(
140
+ 'ai_rpg.interactions',
141
+ {
142
+ 'session_id': pxt.String,
143
+ 'player_name': pxt.String,
144
+ 'genre': pxt.String,
145
+ 'initial_scenario': pxt.String,
146
+ 'turn_number': pxt.Int,
147
+ 'player_input': pxt.String,
148
+ 'timestamp': pxt.Timestamp,
149
+ 'player_stats': pxt.String,
150
+ 'random_event': pxt.String
151
+ }
152
+ )
153
+
154
+ # Add computed columns for AI responses
155
+ interactions.add_computed_column(messages=generate_messages(
156
+ interactions.genre,
157
+ interactions.player_name,
158
+ interactions.initial_scenario,
159
+ interactions.player_input,
160
+ interactions.turn_number,
161
+ interactions.player_stats
162
+ ))
163
+
164
+ interactions.add_computed_column(ai_response=openai.chat_completions(
165
+ messages=interactions.messages,
166
+ model='gpt-4.1-mini',
167
+ max_tokens=800,
168
+ temperature=0.8
169
+ ))
170
+
171
+ interactions.add_computed_column(full_response=interactions.ai_response.choices[0].message.content)
172
+ interactions.add_computed_column(story_text=get_story(interactions.full_response))
173
+ interactions.add_computed_column(stats_update=get_stats_update(interactions.full_response))
174
+ interactions.add_computed_column(options=get_options(interactions.full_response))
175
+
176
+ class RPGGame:
177
+ def __init__(self):
178
+ self.current_session_id = None
179
+ self.turn_number = 0
180
+ self.current_stats = ""
181
+
182
+ def start_game(self, player_name: str, genre: str, scenario: str) -> tuple[str, str, str, list[str]]:
183
+ session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}_{player_name}"
184
+ self.current_session_id = session_id
185
+ self.turn_number = 0
186
+ self.current_stats = initialize_stats(genre)
187
+
188
+ interactions.insert([{
189
+ 'session_id': session_id,
190
+ 'player_name': player_name,
191
+ 'genre': genre,
192
+ 'initial_scenario': scenario,
193
+ 'turn_number': 0,
194
+ 'player_input': "Game starts",
195
+ 'timestamp': datetime.now(),
196
+ 'player_stats': self.current_stats,
197
+ 'random_event': ""
198
+ }])
199
+
200
+ result = interactions.select(
201
+ interactions.story_text,
202
+ interactions.stats_update,
203
+ interactions.options
204
+ ).where(
205
+ (interactions.session_id == session_id) &
206
+ (interactions.turn_number == 0)
207
+ ).collect()
208
+
209
+ return session_id, result['story_text'][0], result['stats_update'][0], result['options'][0]
210
+
211
+ def process_action(self, action: str) -> tuple[str, str, list[str]]:
212
+ if not self.current_session_id:
213
+ return "๊ฒŒ์ž„ ์„ธ์…˜์ด ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ƒˆ ๊ฒŒ์ž„์„ ์‹œ์ž‘ํ•˜์„ธ์š”.", "์Šคํƒฏ ์—†์Œ", []
214
+
215
+ self.turn_number += 1
216
+
217
+ prev_turn = interactions.select(
218
+ interactions.player_name,
219
+ interactions.genre,
220
+ interactions.initial_scenario,
221
+ interactions.player_stats
222
+ ).where(
223
+ (interactions.session_id == self.current_session_id) &
224
+ (interactions.turn_number == self.turn_number - 1)
225
+ ).collect()
226
+
227
+ self.current_stats = prev_turn['player_stats'][0]
228
+ random_event = generate_random_event(self.turn_number)
229
+
230
+ if random_event:
231
+ action = f"{action} ({random_event})"
232
+
233
+ interactions.insert([{
234
+ 'session_id': self.current_session_id,
235
+ 'player_name': prev_turn['player_name'][0],
236
+ 'genre': prev_turn['genre'][0],
237
+ 'initial_scenario': prev_turn['initial_scenario'][0],
238
+ 'turn_number': self.turn_number,
239
+ 'player_input': action,
240
+ 'timestamp': datetime.now(),
241
+ 'player_stats': self.current_stats,
242
+ 'random_event': random_event
243
+ }])
244
+
245
+ result = interactions.select(
246
+ interactions.story_text,
247
+ interactions.stats_update,
248
+ interactions.options
249
+ ).where(
250
+ (interactions.session_id == self.current_session_id) &
251
+ (interactions.turn_number == self.turn_number)
252
+ ).collect()
253
+
254
+ # Update stats for next turn
255
+ self.current_stats = result['stats_update'][0]
256
+
257
+ return result['story_text'][0], result['stats_update'][0], result['options'][0]
258
+
259
+ def create_interface():
260
+ game = RPGGame()
261
+
262
+ # Custom CSS for improved visuals
263
+ custom_css = """
264
+ .container {
265
+ max-width: 1200px;
266
+ margin: 0 auto;
267
+ }
268
+
269
+ .title-container {
270
+ background: linear-gradient(135deg, #6e48aa 0%, #9c27b0 100%);
271
+ color: white;
272
+ padding: 20px;
273
+ border-radius: 15px;
274
+ margin-bottom: 20px;
275
+ text-align: center;
276
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
277
+ }
278
+
279
+ .story-container {
280
+ background: #f8f9fa;
281
+ border-left: 5px solid #9c27b0;
282
+ padding: 15px;
283
+ border-radius: 10px;
284
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
285
+ margin-bottom: 20px;
286
+ font-family: 'Noto Sans KR', sans-serif;
287
+ }
288
+
289
+ .stats-container {
290
+ background: #e8f5e9;
291
+ border-left: 5px solid #4caf50;
292
+ padding: 15px;
293
+ border-radius: 10px;
294
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
295
+ margin-bottom: 20px;
296
+ }
297
+
298
+ .options-container {
299
+ background: #e3f2fd;
300
+ border-left: 5px solid #2196f3;
301
+ padding: 15px;
302
+ border-radius: 10px;
303
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
304
+ margin-bottom: 20px;
305
+ }
306
+
307
+ .action-button {
308
+ background: linear-gradient(135deg, #6e48aa 0%, #9c27b0 100%);
309
+ color: white;
310
+ border: none;
311
+ padding: 10px 20px;
312
+ border-radius: 5px;
313
+ cursor: pointer;
314
+ transition: all 0.3s ease;
315
+ }
316
+
317
+ .action-button:hover {
318
+ transform: translateY(-2px);
319
+ box-shadow: 0 4px 10px rgba(0,0,0,0.2);
320
+ }
321
+
322
+ .history-container {
323
+ background: #fff8e1;
324
+ border-left: 5px solid #ffc107;
325
+ padding: 15px;
326
+ border-radius: 10px;
327
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
328
+ margin-top: 20px;
329
+ }
330
+ """
331
+
332
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
333
+ gr.HTML(
334
+ """
335
+ <div class="title-container">
336
+ <h1 style="margin-bottom: 0.5em; font-size: 2.5em;">๐ŸŽฒ AI RPG ์–ด๋“œ๋ฒค์ฒ˜</h1>
337
+ <p style="font-size: 1.2em;">Pixeltable๊ณผ OpenAI๋กœ ๊ตฌํ˜„๋œ ๋ชฐ์ž…ํ˜• ๋กคํ”Œ๋ ˆ์ž‰ ๊ฒŒ์ž„ ๊ฒฝํ—˜!</p>
338
+ </div>
339
+ """
340
+ )
341
+
342
+ with gr.Row():
343
+ with gr.Column(scale=1):
344
+ with gr.Accordion("๐ŸŽฏ ์ด ์•ฑ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?", open=False):
345
+ gr.HTML(
346
+ """
347
+ <div style="padding: 15px;">
348
+ <h3>AI RPG ์–ด๋“œ๋ฒค์ฒ˜๋Š” ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:</h3>
349
+ <ul style="list-style-type: none; padding-left: 5px;">
350
+ <li>๐ŸŽฎ <b>๋™์  ์Šคํ† ๋ฆฌํ…”๋ง:</b> AI๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ๋ชฐ์ž…ํ˜• ์ด์•ผ๊ธฐ ๊ฒฝํ—˜</li>
351
+ <li>๐Ÿ”„ <b>๊ฒŒ์ž„ ์ƒํƒœ ๊ด€๋ฆฌ:</b> Pixeltable๋กœ ๊ฒŒ์ž„ ์ƒํƒœ์™€ ๊ธฐ๋ก ์ถ”์ </li>
352
+ <li>๐Ÿ’ญ <b>์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์„ ํƒ์ง€:</b> ํ”Œ๋ ˆ์ด์–ด ํ–‰๋™์— ๋”ฐ๋ฅธ ๋งž์ถคํ˜• ์˜ต์…˜</li>
353
+ <li>๐Ÿค– <b>AI ์Šคํ† ๋ฆฌํ…”๋ง:</b> ์ƒ์ƒํ•œ ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ ์ƒ์„ฑ</li>
354
+ <li>๐Ÿ“Š <b>์บ๋ฆญํ„ฐ ์ƒํƒœ ์ถ”์ :</b> ๊ฒŒ์ž„ ์ง„ํ–‰์— ๋”ฐ๋ฅธ ์Šคํƒฏ ๋ณ€ํ™”</li>
355
+ </ul>
356
+ </div>
357
+ """
358
+ )
359
+
360
+ with gr.Accordion("๐ŸŽจ ์บ๋ฆญํ„ฐ ์ƒ์„ฑ", open=True):
361
+ player_name = gr.Textbox(
362
+ label="๐Ÿ‘ค ์บ๋ฆญํ„ฐ ์ด๋ฆ„",
363
+ placeholder="๋‹น์‹ ์˜ ์บ๋ฆญํ„ฐ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”...",
364
+ container=False
365
+ )
366
+ genre = gr.Dropdown(
367
+ choices=[
368
+ "๐Ÿง™โ€โ™‚๏ธ Fantasy",
369
+ "๐Ÿš€ Sci-Fi",
370
+ "๐Ÿ‘ป Horror",
371
+ "๐Ÿ” Mystery",
372
+ "๐ŸŒ‹ Post-Apocalyptic",
373
+ "๐Ÿค– Cyberpunk",
374
+ "โš™๏ธ Steampunk"
375
+ ],
376
+ label="๐ŸŽญ ์žฅ๋ฅด ์„ ํƒ",
377
+ container=False,
378
+ value="๐Ÿง™โ€โ™‚๏ธ Fantasy"
379
+ )
380
+ scenario = gr.Textbox(
381
+ label="๐Ÿ“– ์‹œ์ž‘ ์‹œ๋‚˜๋ฆฌ๏ฟฝ๏ฟฝ๏ฟฝ",
382
+ lines=3,
383
+ placeholder="์ดˆ๊ธฐ ์„ค์ •๊ณผ ์ƒํ™ฉ์„ ์„ค๋ช…ํ•˜์„ธ์š”...",
384
+ container=False
385
+ )
386
+ start_button = gr.Button("๐ŸŽฎ ๋ชจํ—˜ ์‹œ์ž‘!", variant="primary")
387
+
388
+ with gr.Column(scale=2):
389
+ story_display = gr.Markdown(
390
+ label="๐Ÿ“œ ์Šคํ† ๋ฆฌ",
391
+ value="<div class='story-container'>๋ชจํ—˜์„ ์‹œ์ž‘ํ•˜๋ ค๋ฉด ์บ๋ฆญํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  '๋ชจํ—˜ ์‹œ์ž‘!' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์„ธ์š”.</div>",
392
+ show_label=False
393
+ )
394
+
395
+ stats_display = gr.Markdown(
396
+ label="๐Ÿ“Š ์บ๋ฆญํ„ฐ ์Šคํƒฏ",
397
+ value="<div class='stats-container'>๋ชจํ—˜์„ ์‹œ์ž‘ํ•˜๋ฉด ์บ๋ฆญํ„ฐ ์Šคํƒฏ์ด ์ด๊ณณ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</div>",
398
+ show_label=False
399
+ )
400
+
401
+ gr.HTML("<div class='options-container'><h3>๐ŸŽฏ ๋‹ค์Œ ํ–‰๋™ ์„ ํƒ</h3></div>")
402
+
403
+ action_input = gr.Radio(
404
+ choices=[],
405
+ label="",
406
+ interactive=True
407
+ )
408
+ submit_action = gr.Button("โšก ํ–‰๋™ ์‹คํ–‰", variant="primary")
409
+
410
+ with gr.Row():
411
+ with gr.Column():
412
+ gr.HTML("<div class='history-container'><h3>๐Ÿ’ซ ์–ด๋“œ๋ฒค์ฒ˜ ์˜ˆ์‹œ</h3></div>")
413
+ gr.Examples(
414
+ examples=[
415
+ ["์ด์ˆœ์‹ ", "๐Ÿง™โ€โ™‚๏ธ Fantasy", "๋‹น์‹ ์€ ์žŠํ˜€์ง„ ์‹ ๋น„ํ•œ ์ˆฒ์˜ ๊ฐ€์žฅ์ž๋ฆฌ์—์„œ ๋ˆˆ์„ ๋œน๋‹ˆ๋‹ค. ๋ฉ€๋ฆฌ์„œ ์„ฑ์˜ ์ฒจํƒ‘์ด ๋ณด์ด๊ณ , ๋‹น์‹ ์˜ ๋จธ๋ฆฌ์†์—๋Š” ์™•๊ตญ์„ ์œ„ํ˜‘ํ•˜๋Š” ๊ณ ๋Œ€ ๋งˆ๋ฒ•์— ๊ด€ํ•œ ๋‹จ์„œ๋งŒ์ด ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ‘์ž๊ธฐ ์ˆฒ์—์„œ ์ด์ƒํ•œ ๋น›์ด ๋ณด์ž…๋‹ˆ๋‹ค..."],
416
+ ["๊น€์ง€์˜", "๐Ÿš€ Sci-Fi", "์šฐ์ฃผ์„  'ํ˜ธ๋ผ์ด์ฆŒ'์˜ ํ•ญํ•ด์‚ฌ๋กœ์„œ, ๋‹น์‹ ์€ ๋ฏธ์ง€์˜ ํ–‰์„ฑ ํƒ์‚ฌ ์ค‘ ๋น„์ƒ ์•Œ๋žŒ์— ๊นจ์–ด๋‚ฉ๋‹ˆ๋‹ค. ์„ ์žฅ๊ณผ ์—ฐ๋ฝ์ด ๋‘์ ˆ๋˜์—ˆ๊ณ , ์ƒ๋ช… ์œ ์ง€ ์‹œ์Šคํ…œ์ด ์ ์ฐจ ์‹คํŒจํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์กฐ์šฉํ•œ ์„ ๋‚ด์—์„œ ์ด์ƒํ•œ ๋ฐœ๊ฑธ์Œ ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฝ๋‹ˆ๋‹ค..."],
417
+ ["์ผ๋ก  ๋จธ์Šคํฌ", "๐Ÿค– Cyberpunk", "2077๋…„ ์„œ์šธ, ๋‹น์‹ ์€ ๋‰ด๋Ÿด๋งํฌ ์ธ๋”์ŠคํŠธ๋ฆฌ์˜ CEO์ž…๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์ตœ์‹  ๋‡Œ-์ปดํ“จํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ์ˆ ์ด ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋Šฅ๋ ฅ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค - ๊ทธ๋“ค์˜ ์ง‘์•ˆ ์‹๋ฌผ๊ณผ ๊ต๊ฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ํˆฌ์ž์ž ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ค€๋น„ํ•˜๋Š” ๋™์•ˆ, AI ๋น„์„œ๊ฐ€ ํ…Œ์ŠคํŠธ ์ฐธ๊ฐ€์ž๋“ค์ด ๋ถˆ๊ฐ€์‚ฌ์˜ํ•œ '์‹๋ฌผ ํ˜๋ช…'์„ ์กฐ์งํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๋ณด๊ณ ๋ฅผ ์ „ํ•ฉ๋‹ˆ๋‹ค..."],
418
+ ["๊ณ ๋“  ๋žจ์ง€", "๐ŸŒ‹ Post-Apocalyptic", "๋‹น์‹ ์€ ๋‰ด ์„œ์šธ์˜ ๋งˆ์ง€๋ง‰ ๋งˆ์Šคํ„ฐ ์…ฐํ”„๋กœ, ์˜› ๋Ÿญ์…”๋ฆฌ ํ˜ธํ…” ํํ—ˆ์—์„œ ์ง€ํ•˜ ๋ ˆ์Šคํ† ๋ž‘์„ ์šด์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์š”๋ฆฌ๋Š” ์œ„ํ—˜ํ•œ ๋ฐฉ์‚ฌ๋Šฅ ๊ตฌ์—ญ์—์„œ๋งŒ ์ž๋ผ๋Š” ํฌ๊ท€ ๋ฒ„์„ฏ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋Š˜ ๋ฐค์˜ ๋น„๋ฐ€ ๋ชจ์ž„์„ ์ค€๋น„ํ•˜๋˜ ์ค‘, ์ •์ฐฐ๋ณ‘์ด ์ฃผ๋ณ€ ์ง€์—ญ์˜ ๊ฒฝ์Ÿ ์š”๋ฆฌ์‚ฌ ๊ฐฑ๋‹จ์— ๊ด€ํ•œ ๋ถˆ๊ธธํ•œ ์†Œ์‹์„ ๊ฐ€์ง€๊ณ  ๋Œ์•„์˜ต๋‹ˆ๋‹ค..."],
419
+ ["์œ ์ •ํ˜ธ", "๐Ÿ‘ป Horror", "๋‹น์‹ ์€ ์นœ๊ตฌ์˜ ์ดˆ๋Œ€๋กœ ์‚ผ๋ฆผ ์† ์™ธ๋”ด ๋ณ„์žฅ์— ์ฃผ๋ง์„ ๋ณด๋‚ด๋Ÿฌ ์™”์Šต๋‹ˆ๋‹ค. ์ฒซ๋‚  ๋ฐค, ์ฐฝ๋ฐ–์œผ๋กœ ๋ณด์ด๋Š” ๊ธฐ์ดํ•œ ๋น›์— ์ด๋Œ๋ ค ์ˆฒ์œผ๋กœ ๋“ค์–ด๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋Œ์•„์˜ค๋Š” ๊ธธ์„ ์ฐพ์œผ๋ ค ํ•˜์ง€๋งŒ, ๋ณ„์žฅ์ด ๋ณด์ด์ง€ ์•Š๊ณ  ๋‚ฏ์„  ์•ˆ๊ฐœ๊ฐ€ ์ ์  ์ง™์–ด์ง‘๋‹ˆ๋‹ค. ๋ฉ€๋ฆฌ์„œ ๋ˆ„๊ตฐ๊ฐ€โ€”์•„๋‹ˆ, ๋ฌด์–ธ๊ฐ€๊ฐ€ ๋‹น์‹ ์„ ๋ถ€๋ฅด๋Š” ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฝ๋‹ˆ๋‹ค..."],
420
+ ["๋ฐ•์ง€ํ›ˆ", "๐Ÿ” Mystery", "์‹ ์ž„ ํ˜•์‚ฌ๋กœ์„œ ๋‹น์‹ ์˜ ์ฒซ ์‚ฌ๊ฑด์€ ๋„์‹œ ์ตœ๊ณ ์˜ ๊ธฐ์ˆ  ๊ธฐ์—… CEO์˜ ์˜๋ฌธ์˜ ์‹ค์ข…์ž…๋‹ˆ๋‹ค. ๊ทธ์˜ ์‚ฌ๋ฌด์‹ค์—๋Š” ํ˜ˆํ”์ด ์—†๊ณ , ์œ ์ผํ•œ ๋‹จ์„œ๋Š” ์ฑ…์ƒ ์œ„์— ๋†“์ธ ์•”ํ˜ธํ™”๋œ ๋ฉ”๋ชจ์™€ ๊บผ์ ธ์žˆ๋Š” ๊ทธ์˜ ์ตœ์ฒจ๋‹จ AI ๋น„์„œ๋ฟ์ž…๋‹ˆ๋‹ค. ์กฐ์‚ฌ๋ฅผ ์‹œ์ž‘ํ•˜์ž๋งˆ์ž, ๋‹น์‹ ์€ CEO๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž‘์—…ํ•˜๋˜ ๋น„๋ฐ€ ํ”„๋กœ์ ํŠธ์— ๊ด€ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋“ฃ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค..."],
421
+ ["์ด๋ฏผ์ˆ˜", "โš™๏ธ Steampunk", "์ฆ๊ธฐ์™€ ๊ธฐ์–ด๋กœ ๊ฐ€๋“ํ•œ ๋‰ด ์กฐ์„ ์—์„œ, ๋‹น์‹ ์€ ํ˜์‹ ์ ์ธ ๋น„ํ–‰์„  ์„ค๊ณ„์ž์ž…๋‹ˆ๋‹ค. ๋‹น์‹ ์˜ ์ตœ์‹  ๋ฐœ๋ช…ํ’ˆ ์‹œ์—ฐ ์ค‘, ์ •๋ถ€์˜ ๋น„๋ฐ€ ์š”์›์ด ์ ‘๊ทผํ•ด ์œ„ํ—˜์— ์ฒ˜ํ•œ ํ™ฉ์‹ค ๊ฐ€์กฑ์„ ์œ„ํ•œ ๋น„๋ฐ€ ์ž„๋ฌด๋ฅผ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค. ์ง€ํ•˜ ๋‹จ์ฒด๋“ค์ด ์™•์ขŒ๋ฅผ ์œ„ํ˜‘ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋‹น์‹ ์˜ ๋ฐœ๋ช…ํ’ˆ์ด ์™•๊ฐ€์˜ ์œ ์ผํ•œ ํฌ๋ง์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค..."]
422
+ ],
423
+ inputs=[player_name, genre, scenario]
424
+ )
425
+
426
+ with gr.Column():
427
+ history_df = gr.Dataframe(
428
+ headers=["๐Ÿ“… ํ„ด", "๐ŸŽฏ ํ”Œ๋ ˆ์ด์–ด ํ–‰๋™", "๐Ÿ’ฌ ๊ฒŒ์ž„ ๋ฐ˜์‘"],
429
+ label="๐Ÿ“š ๋ชจํ—˜ ์—ญ์‚ฌ",
430
+ wrap=True,
431
+ row_count=5,
432
+ col_count=(3, "fixed")
433
+ )
434
+
435
+ def start_new_game(name, genre_choice, scenario_text):
436
+ if not name or not genre_choice or not scenario_text:
437
+ return (
438
+ "<div class='story-container'>๋ชจ๋“  ํ•„๋“œ๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ์‹œ์ž‘ํ•˜์„ธ์š”.</div>",
439
+ "<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
440
+ [],
441
+ []
442
+ )
443
+
444
+ try:
445
+ _, initial_story, initial_stats, initial_options = game.start_game(name, genre_choice, scenario_text)
446
+
447
+ history_df = interactions.select(
448
+ turn=interactions.turn_number,
449
+ action=interactions.player_input,
450
+ response=interactions.story_text
451
+ ).where(
452
+ interactions.session_id == game.current_session_id
453
+ ).order_by(
454
+ interactions.turn_number
455
+ ).collect().to_pandas()
456
+
457
+ history_data = [
458
+ [str(row['turn']), row['action'], row['response']]
459
+ for _, row in history_df.iterrows()
460
+ ]
461
+
462
+ story_html = f"<div class='story-container'>{initial_story}</div>"
463
+ stats_html = f"<div class='stats-container'><h3>๐Ÿ“Š ์บ๋ฆญํ„ฐ ์ƒํƒœ</h3>{initial_stats}</div>"
464
+
465
+ return story_html, stats_html, gr.Radio(choices=initial_options, interactive=True), history_data
466
+ except Exception as e:
467
+ return (
468
+ f"<div class='story-container'>๊ฒŒ์ž„ ์‹œ์ž‘ ์˜ค๋ฅ˜: {str(e)}</div>",
469
+ "<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
470
+ [],
471
+ []
472
+ )
473
+
474
+ def process_player_action(action_choice):
475
+ try:
476
+ if not action_choice:
477
+ return (
478
+ "<div class='story-container'>๊ณ„์†ํ•˜๋ ค๋ฉด ํ–‰๋™์„ ์„ ํƒํ•˜์„ธ์š”.</div>",
479
+ "<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
480
+ [],
481
+ []
482
+ )
483
+
484
+ story, stats, options = game.process_action(action_choice)
485
+
486
+ history_df = interactions.select(
487
+ turn=interactions.turn_number,
488
+ action=interactions.player_input,
489
+ response=interactions.story_text
490
+ ).where(
491
+ interactions.session_id == game.current_session_id
492
+ ).order_by(
493
+ interactions.turn_number
494
+ ).collect().to_pandas()
495
+
496
+ history_data = [
497
+ [str(row['turn']), row['action'], row['response']]
498
+ for _, row in history_df.iterrows()
499
+ ]
500
+
501
+ story_html = f"<div class='story-container'>{story}</div>"
502
+ stats_html = f"<div class='stats-container'><h3>๐Ÿ“Š ์บ๋ฆญํ„ฐ ์ƒํƒœ</h3>{stats}</div>"
503
+
504
+ return story_html, stats_html, gr.Radio(choices=options, interactive=True), history_data
505
+ except Exception as e:
506
+ return (
507
+ f"<div class='story-container'>์˜ค๋ฅ˜: {str(e)}</div>",
508
+ "<div class='stats-container'>์Šคํƒฏ ์ •๋ณด ์—†์Œ</div>",
509
+ [],
510
+ []
511
+ )
512
+
513
+ start_button.click(
514
+ start_new_game,
515
+ inputs=[player_name, genre, scenario],
516
+ outputs=[story_display, stats_display, action_input, history_df]
517
+ )
518
+
519
+ submit_action.click(
520
+ process_player_action,
521
+ inputs=[action_input],
522
+ outputs=[story_display, stats_display, action_input, history_df]
523
+ )
524
+
525
+ gr.HTML("""
526
+ <div style="text-align: center; margin-top: 30px; padding: 20px; background: #f5f5f5; border-radius: 10px;">
527
+ <h3>๐ŸŒŸ AI RPG ์–ด๋“œ๋ฒค์ฒ˜ - Pixeltable๋กœ ์ œ์ž‘๋œ ๋ชฐ์ž…ํ˜• ๋กคํ”Œ๋ ˆ์ž‰ ๊ฒฝํ—˜</h3>
528
+ <p>์ž์‹ ๋งŒ์˜ ์บ๋ฆญํ„ฐ๋ฅผ ๋งŒ๋“ค๊ณ , ์„ ํƒํ•œ ์žฅ๋ฅด์˜ ์„ธ๊ณ„์—์„œ ๋ชจํ—˜์„ ์ฆ๊ธฐ์„ธ์š”. ๋‹น์‹ ์˜ ์„ ํƒ์ด ์Šคํ† ๋ฆฌ๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค!</p>
529
+ </div>
530
+ """)
531
+
532
+ return demo
533
+
534
+ if __name__ == "__main__":
535
+ demo = create_interface()
536
+ demo.launch()
537
+