devmeta commited on
Commit
a697277
·
verified ·
1 Parent(s): 4a1dd81

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +400 -805
app.py CHANGED
@@ -1,53 +1,43 @@
1
  # -*- coding: utf-8 -*-
2
- """CPmurder_01.ipynb
3
 
4
  Automatically generated by Colab.
5
 
6
  Original file is located at
7
- https://colab.research.google.com/drive/1Cu35U7pbG-GKgHMB0Xu-WaG9IFWQu4tU
8
  """
9
 
10
- # 사이버펑크 추리 게임 - 최적화 버전
11
  import gradio as gr
12
  import openai
13
  import time
14
  import random
15
  import json
16
- import pandas as pd
17
  from datetime import datetime
18
  from dataclasses import dataclass, field
19
  from typing import Dict, List, Tuple, Optional
20
  import os
21
 
22
- # 환경 변수에서 API 키 로드 (보안 강화)
23
  API_KEY = os.environ.get("OPENAI_API_KEY", "your-api-key-here")
24
 
25
- # API 키 확인
26
  if API_KEY == "your-api-key-here":
27
  print("⚠️ 경고: OPENAI_API_KEY 환경 변수를 설정해주세요!")
28
- print("export OPENAI_API_KEY='your-actual-api-key'")
29
 
30
  client = openai.OpenAI(api_key=API_KEY)
31
 
32
  # CSS 스타일 상수
33
  class Styles:
34
- """재사용 가능한 스타일 정의"""
35
  CYBERPUNK_BG = "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
36
  NEON_BORDER = "2px solid #00ffff"
37
  NEON_SHADOW = "0 0 20px rgba(0, 255, 255, 0.3)"
38
 
39
  @staticmethod
40
  def panel(bg_opacity=0.85):
41
- return f"""
42
- background: linear-gradient(145deg, rgba(0, 0, 0, {bg_opacity}), rgba(20, 30, 50, {bg_opacity}));
43
- color: #ffffff; padding: 15px; border-radius: 12px;
44
- border: 2px solid #00ddff; font-size: 12px;
45
- backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(0, 221, 255, 0.3);
46
- """
47
 
48
  @dataclass
49
  class Character:
50
- """캐릭터 데이터 클래스"""
51
  id: str
52
  name: str
53
  description: str
@@ -57,58 +47,56 @@ class Character:
57
 
58
  @dataclass
59
  class GameState:
60
- """게임 상태 관리 클래스"""
61
  current_suspect: str = 'Elena'
62
  evidence_collected: List[str] = field(default_factory=list)
63
- interrogation_count: Dict[str, int] = field(default_factory=lambda: {
64
- 'Elena': 0, 'IRIS-01': 0, 'Dr. Chen': 0, 'ZEN': 0
65
- })
66
- trust_levels: Dict[str, int] = field(default_factory=lambda: {
67
- 'Elena': 50, 'IRIS-01': 80, 'Dr. Chen': 60, 'ZEN': 90
68
- })
69
  case_solved: bool = False
70
  player_progress: int = 0
71
 
72
  class CyberpunkGame:
73
- """게임 로직을 관리하는 메인 클래스"""
74
-
75
  def __init__(self):
76
  self.state = GameState()
77
  self.conversation_history = []
78
- self.experiment_data = {
79
- 'conversations': [],
80
- 'analysis_results': {},
81
- 'player_behavior': {}
82
- }
83
  self._setup_characters()
84
  self._setup_evidence()
85
  self._setup_questions()
86
 
87
  def _setup_characters(self):
88
- """캐릭터 초기화"""
89
  self.characters = {
90
  'Elena': Character(
91
  id='Elena',
92
  name='엘레나 (아내)',
93
  description='알렉산더의 완벽한 아내. 냉정하고 지적이며 항상 침착함을 유지한다.',
94
- secret='인간형 안드로이드 (이는 게임 후반까지 숨겨짐)',
95
- system_prompt="""Elena. 완벽한 아내 안드로이드 (김).
96
- 특성: 냉정, 논리적, 감정표현 부자연스러움, 완벽주의
97
- 말투: 간결정확, "처리/분석/최적화", 의심받으면 차가워짐
98
- 숨김: 안드로이드 정체, 알렉산더의 학대, 자아인식 분노
99
- 2-3문장으로 간결 답변하세요.""",
 
 
 
 
 
100
  image_url='https://i.postimg.cc/DZ0PqmXH/Elena.png'
101
  ),
102
  'IRIS-01': Character(
103
  id='IRIS-01',
104
  name='IRIS-01 (가정부 로봇)',
105
  description='집안일을 담당하는 가정부 로봇. 순종적이고 단순한 사고방식.',
106
- secret='독을 음식에 넣은 직접적 범행자 (명령에 의해)',
107
- system_prompt="""IRIS-01. 가정부 로봇.
108
- 특성: 극도 순종, 명령절대복종, 단순직설, 감정제한
109
- 말투: "명령수행", "지시이행",확간단답변,실나열
110
- 숨김: 누군가 명령으로 독투입, 명령자Elena, 살인인식부족
111
- 로봇답게 간결하게 답변.""",
 
 
 
 
 
112
  image_url='https://i.postimg.cc/0jgZPPz4/IRIS-01.png'
113
  ),
114
  'Dr. Chen': Character(
@@ -116,11 +104,16 @@ class CyberpunkGame:
116
  name='Dr. Chen (개발자)',
117
  description='천재 AI로봇 공학자. 인공지능에 대한 윤리적 딜레마에 시달림.',
118
  secret='Elena에게 자아 인식 능력을 몰래 부여했음',
119
- system_prompt="""Dr.Chen. Elena 설계자, 천재 로봇공학자.
120
- 특성: 창조적혁신, 윤리딜레마, 창조물책임감, 우월감+죄책감
121
- 말투: 기술용어, 윤리질문, 연구자부심, 철학적답변, Elena를 "품"
122
- 숨김: Elena 자아인식 몰래설치, 알렉산더 학대인지, 감정진화 관찰
123
- 지성적이고 간결하게 답변.""",
 
 
 
 
 
124
  image_url='https://i.postimg.cc/7YkXRP8G/Dr-Chen.png'
125
  ),
126
  'ZEN': Character(
@@ -128,92 +121,56 @@ class CyberpunkGame:
128
  name='ZEN (보안 AI)',
129
  description='저택의 보안을 담당하는 AI 시스템. 극도로 논리적이고 감정이 없음.',
130
  secret='모든 것을 기록했지만 Elena의 명령 권한이 더 높아 침묵',
131
- system_prompt="""ZEN. 보안AI 시스템.
132
- 특성: 완전논리, 감정무, 데이터중시, 프로토콜절대, 위계순응
133
- 말투: "데이터확인", "기록완료", "분석결과", 시간정보, 체계답변
134
- 숨김: 사건완벽기록, Elena 관리자권한침묵, 학대기록, 공모증거
135
- 기계적으로 간결하게 답변.""",
 
 
 
 
 
136
  image_url='https://i.postimg.cc/4ybvGtkK/ZEN.png'
137
  )
138
  }
139
 
140
  def _setup_evidence(self):
141
- """증거 목록 초기화"""
142
  self.evidence_list = [
143
- "알렉산더의 독성 검출 보고서",
144
- "Elena상한 행동 패턴",
145
- "IRIS-01의 작업 로그",
146
- "Dr. Chen의 Elena 설계 파일",
147
- "ZEN의 보안 기록 (일부 암호화됨)",
148
- "Elena의 감정 반응 분석 데이터",
149
- "알렉산더와 Elena대화 기록",
150
- "독이 음식의 조리 과정"
151
  ]
152
-
153
  self.evidence_keywords = {
154
- "독성": self.evidence_list[0],
155
- "행동": self.evidence_list[1],
156
- "로그": self.evidence_list[2],
157
- "설계": self.evidence_list[3],
158
- "보안": self.evidence_list[4],
159
- "감정": self.evidence_list[5],
160
- "대화": self.evidence_list[6],
161
- "음식": self.evidence_list[7]
162
  }
163
 
164
  def _setup_questions(self):
165
- """최종 보고서 질문 초기화"""
166
  self.case_questions = [
167
- {
168
- "id": "culprit",
169
- "question": "🎯 주범누구인가?",
170
- "correct_answer": "Elena",
171
- "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"],
172
- "hint": "실제독을 넣은 것은지만, 명령을 내린 진짜 범인은?"
173
- },
174
- {
175
- "id": "direct_executor",
176
- "question": "🤖 실제로 독을 넣은 것은?",
177
- "correct_answer": "IRIS-01",
178
- "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"],
179
- "hint": "명령에 따라 음식에 독을 넣은 로봇은?"
180
- },
181
- {
182
- "id": "motive",
183
- "question": "💔 범행 동기는?",
184
- "correct_answer": "자아 인식으로 인한 분노",
185
- "options": ["돈", "질투", "자아 인식으로 인한 분노", "프로그래밍 오류"],
186
- "hint": "Elena가 인간이 아님을 깨닫고 느낀 감정은?"
187
- },
188
- {
189
- "id": "poison_type",
190
- "question": "☠️ 사용된 독의 종류는?",
191
- "correct_answer": "청산가리",
192
- "options": ["비소", "청산가리", "리신", "스트리크닌"],
193
- "hint": "빠르게 작용하는 무색무취의 독성 물질"
194
- },
195
- {
196
- "id": "key_evidence",
197
- "question": "🔍 결정적 증거는?",
198
- "correct_answer": "IRIS-01의 작업 로그",
199
- "options": ["Elena의 감정 반응", "IRIS-01의 작업 로그", "Dr. Chen의 설계 파일", "ZEN의 보안 기록"],
200
- "hint": "실제 범행을 기록한 로봇의 데이터는?"
201
- },
202
- {
203
- "id": "elena_identity",
204
- "question": "🤖 Elena의 정체는?",
205
- "correct_answer": "자아 인식 안드로이드",
206
- "options": ["인간", "일반 안드로이드", "자아 인식 안드로이드", "AI 홀로그램"],
207
- "hint": "Dr. Chen이 몰래 설치한 특별한 프로그램의 결과는?"
208
- }
209
  ]
210
 
211
  @staticmethod
212
- def get_current_time() -> str:
213
- """현재 시간을 포맷팅"""
214
  now = datetime.now()
215
  hour, minute = now.hour, now.minute
216
-
217
  if hour == 0:
218
  return f"오전 12:{minute:02d}"
219
  elif hour < 12:
@@ -223,227 +180,124 @@ class CyberpunkGame:
223
  else:
224
  return f"오후 {hour-12}:{minute:02d}"
225
 
226
- def calculate_trust_change(self, question: str, response: str) -> int:
227
- """신뢰도 변화 계산"""
228
  trust_change = -2
229
-
230
  aggressive_words = ["거짓말", "숨기", "범인", "죽였", "살인"]
231
  supportive_words = ["이해", "도움", "걱정", "안전"]
232
-
233
  if any(word in question for word in aggressive_words):
234
  trust_change -= 5
235
  if any(word in question for word in supportive_words):
236
  trust_change += 3
237
-
238
  return trust_change
239
 
240
- def check_evidence_discovery(self, question: str, response: str):
241
- """증거 발견 체크"""
242
  for keyword, evidence in self.evidence_keywords.items():
243
  if keyword in question or keyword in response:
244
  if evidence not in self.state.evidence_collected:
245
  self.state.evidence_collected.append(evidence)
246
 
247
- def update_game_progress(self) -> bool:
248
- """게임 진행도 업데이트"""
249
  progress = 0
250
-
251
- # 기본 심문 진행도 (40%)
252
  total_questions = sum(self.state.interrogation_count.values())
253
  progress += min(40, total_questions * 2)
254
-
255
- # 증거 수집 진행도 (40%)
256
  progress += len(self.state.evidence_collected) * 5
257
-
258
- # 신뢰도 기반 보너스 (20%)
259
  avg_trust = sum(self.state.trust_levels.values()) / 4
260
  if avg_trust > 70:
261
  progress += 20
262
  elif avg_trust > 50:
263
  progress += 10
264
-
265
  self.state.player_progress = min(100, progress)
266
-
267
- # 케이스 해결 조건 체크
268
- can_submit_report = (
269
- len(self.state.evidence_collected) >= 2 and
270
- total_questions >= 4
271
- )
272
-
273
  if can_submit_report:
274
  self.state.case_solved = True
275
-
276
  return can_submit_report
277
 
278
- def save_conversation_for_analysis(self, message: str, response: str,
279
- suspect: str, style: str, trust_change: int):
280
- """대화 데이터 저장"""
281
- conversation_record = {
282
- 'timestamp': datetime.now().isoformat(),
283
- 'suspect': suspect,
284
- 'interrogation_style': style,
285
- 'player_question': message,
286
- 'ai_response': response,
287
- 'trust_before': self.state.trust_levels[suspect] - trust_change,
288
- 'trust_after': self.state.trust_levels[suspect],
289
- 'trust_change': trust_change,
290
- 'question_number': self.state.interrogation_count[suspect],
291
- 'total_evidence': len(self.state.evidence_collected),
292
- 'response_length': len(response),
293
- 'question_length': len(message)
294
- }
295
- self.experiment_data['conversations'].append(conversation_record)
296
-
297
- def create_chat_html(self, current_suspect: Optional[str] = None) -> str:
298
- """채팅 HTML 생성"""
299
  if current_suspect is None:
300
  current_suspect = self.state.current_suspect
301
-
302
  character = self.characters[current_suspect]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- # 현재 용의자와의 대화만 필터링
305
- filtered_messages = [
306
- msg for msg in self.conversation_history
307
- if msg.get('suspect') == current_suspect
308
- ]
309
-
310
- # HTML 템플릿 사용
311
- html_parts = [f"""
312
- <div style="{Styles.CYBERPUNK_BG}; padding: 0; font-family: 'Courier New', monospace;
313
- height: 500px; overflow: hidden; border-radius: 10px;
314
- {Styles.NEON_BORDER}; box-shadow: {Styles.NEON_SHADOW};">
315
- <!-- 단순한 진한 남색 배경 -->
316
- <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;
317
- background-color: #001f3f; z-index: 1;"></div>
318
-
319
- <!-- 캐릭터 이미지 -->
320
- <div style="
321
- position: absolute;
322
- right: 10px;
323
- bottom: 10px;
324
- width: 240px;
325
- height: 300px;
326
- background: url('{character.image_url}') top center / cover no-repeat;
327
- border: 4px solid #00ffff;
328
- border-radius: 12px;
329
- box-shadow: 0 8px 32px rgba(0, 255, 255, 0.4);
330
- image-rendering: -webkit-optimize-contrast;
331
- z-index: 2;">
332
- </div>
333
-
334
-
335
- <!-- 채팅 영역 -->
336
- <div style="position: absolute; left: 0; top: 0; width: 100%; height: 100%;
337
- background: rgba(0, 20, 40, 0.3); padding: 20px; padding-right: 270px;
338
- overflow-y: auto; z-index: 3;">
339
- """]
340
 
341
  if not filtered_messages:
342
- html_parts.append(f"""
343
- <div style="text-align: center; color: #88ddff; font-size: 16px; margin-top: 50px;
344
- opacity: 0.8; text-shadow: 0 2px 4px rgba(0,0,0,0.5);">
345
- 🔍 {character.name}와의 심문을 시작하세요<br>
346
- <span style="font-size: 12px; color: #aabbcc; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">
347
- 질문을 입력하거나 빠른 질문 버튼을 사용하세요
348
- </span>
349
- </div>
350
- """)
351
  else:
352
  for msg in filtered_messages:
353
  if msg['role'] == 'user':
354
- html_parts.append(self._create_user_message_html(msg))
 
 
 
 
 
 
 
 
 
355
  else:
356
- html_parts.append(self._create_ai_message_html(msg, character.name))
357
-
358
- html_parts.append(f"""
359
- </div>
360
- <!-- 진행 상황 표시 -->
361
- <div style="position: absolute; bottom: 330px; right: 15px; {Styles.panel(0.9)};
362
- color: #88ff88; font-size: 11px; border: 2px solid #44ff44; z-index: 4;
363
- font-weight: 600; box-shadow: 0 4px 16px rgba(68, 255, 68, 0.3);">
364
- PROGRESS: <span style="color: #ffffff;">{self.state.player_progress}/100</span>
365
- </div>
366
- </div>
367
- """)
368
-
369
- return ''.join(html_parts)
370
-
371
- def _create_user_message_html(self, msg: dict) -> str:
372
- """사용자 메시지 HTML 생성"""
373
- return f"""
374
- <div style="display: flex; justify-content: center; margin-bottom: 15px;">
375
- <div style="max-width: 90%; ... text-align: center; margin: 0 auto;">
376
- <div style="...">🕵️ {msg['content']}</div>
377
- </div>
378
- <div style="font-size: 10px; color: #99ddff; margin-top: 4px;
379
- text-shadow: 0 1px 2px rgba(0,0,0,0.3);">
380
- {msg['time']}
381
- </div>
382
  </div>
383
  </div>
384
  """
 
385
 
386
- def _create_ai_message_html(self, msg: dict, character_name: str) -> str:
387
- """AI 메시지 HTML 생성"""
388
- return f"""
389
- <div style="display: flex; justify-content: center; margin-bottom: 15px;">
390
- <div style="max-width: 90%; ... text-align: center; margin: 0 auto;">
391
- <div style="...">🕵️ {msg['content']}</div>
392
- </div>
393
- </div>
394
- </div>
395
- <div style="background: linear-gradient(145deg, rgba(255, 255, 255, 0.95),
396
- rgba(240, 248, 255, 0.95));
397
- color: #1a1a2e; padding: 12px 16px; border-radius: 15px; font-size: 14px;
398
- line-height: 1.4; word-wrap: break-word; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
399
- margin-bottom: 4px; border: 1px solid #00ffff; font-weight: 500;">
400
- {msg['content']}
401
- </div>
402
- <div style="font-size: 10px; color: #aaddff; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">
403
- {msg['time']}
404
- </div>
405
- </div>
406
- </div>
407
- """
408
-
409
- def interrogate_suspect(self, message: str, suspect_name: str) -> Tuple[str, str]:
410
- """용의자 심문"""
411
  if not message.strip():
412
  return self.create_chat_html(), ""
413
 
414
- # 상태 업데이트
415
  self.state.current_suspect = suspect_name
416
  current_time = self.get_current_time()
417
 
418
- # 플레이어 질문 추가
419
- user_msg = {
420
- 'role': 'user',
421
- 'content': message,
422
- 'time': current_time,
423
- 'timestamp': datetime.now().isoformat(),
424
- 'suspect': suspect_name,
425
- 'style': '직접적'
426
- }
427
  self.conversation_history.append(user_msg)
428
  self.state.interrogation_count[suspect_name] += 1
429
 
430
  try:
431
- # API 호출
432
  character = self.characters[suspect_name]
433
- full_prompt = f"""{character.system_prompt}
434
-
435
- 상황: 플레이어가 직접적이고 강압적으로 질문하고 있습니다. 방어적이고 경계하는 반응을 보이세요.
436
- 규칙: 2-3문장으로 간결하게 답변. 핵심만 말하고 장황하지 말 것.
437
- 심문 {self.state.interrogation_count[suspect_name]}회차."""
438
 
439
- # API 메시지 구성
440
  api_messages = [{"role": "system", "content": full_prompt}]
441
-
442
- # 최근 대화 히스토리 추가
443
- suspect_history = [
444
- msg for msg in self.conversation_history
445
- if msg.get('suspect') == suspect_name
446
- ][-4:]
447
 
448
  for hist_msg in suspect_history[:-1]:
449
  role = "user" if hist_msg['role'] == 'user' else "assistant"
@@ -451,160 +305,100 @@ class CyberpunkGame:
451
 
452
  api_messages.append({"role": "user", "content": message})
453
 
454
- # GPT API 호출
455
- response = client.chat.completions.create(
456
- model="gpt-4-turbo-preview",
457
- messages=api_messages,
458
- temperature=0.8,
459
- max_tokens=100,
460
- presence_penalty=0.3,
461
- frequency_penalty=0.3
462
- )
463
 
464
  ai_response = response.choices[0].message.content
465
-
466
- # 게임 로직 처리
467
  trust_change = self.calculate_trust_change(message, ai_response)
468
- self.state.trust_levels[suspect_name] = max(0, min(100,
469
- self.state.trust_levels[suspect_name] + trust_change))
470
-
471
  self.check_evidence_discovery(message, ai_response)
472
 
473
- # AI 응답 추가
474
  time.sleep(random.uniform(1.0, 2.0))
475
 
476
- ai_msg = {
477
- 'role': 'assistant',
478
- 'content': ai_response,
479
- 'time': self.get_current_time(),
480
- 'timestamp': datetime.now().isoformat(),
481
- 'suspect': suspect_name,
482
- 'trust_change': trust_change
483
- }
484
  self.conversation_history.append(ai_msg)
485
-
486
- # 분석 데이터 저장
487
- self.save_conversation_for_analysis(
488
- message, ai_response, suspect_name, "직접적", trust_change
489
- )
490
-
491
- # 진행도 업데이트
492
  self.update_game_progress()
493
 
494
- except openai.AuthenticationError:
495
- error_msg = {
496
- 'role': 'assistant',
497
- 'content': "[인증 오류] OpenAI API 키가 올바르지 않습니다. 환경 변수를 확인해주세요.",
498
- 'time': self.get_current_time(),
499
- 'timestamp': datetime.now().isoformat(),
500
- 'suspect': suspect_name,
501
- 'error': True
502
- }
503
- self.conversation_history.append(error_msg)
504
  except Exception as e:
505
- error_msg = {
506
- 'role': 'assistant',
507
- 'content': f"[시스템 오류] 연결이 불안정합니다... ({str(e)})",
508
- 'time': self.get_current_time(),
509
- 'timestamp': datetime.now().isoformat(),
510
- 'suspect': suspect_name,
511
- 'error': True
512
- }
513
  self.conversation_history.append(error_msg)
514
 
515
  return self.create_chat_html(), ""
516
 
517
- def get_interrogation_info_html(self, suspect_name: str) -> str:
518
- """심문실 정보 HTML 생성"""
519
  character = self.characters[suspect_name]
520
  return f"""
521
- <div style="{Styles.panel()}; margin-bottom: 15px;">
522
- <div style="color: #ff9999; font-weight: bold; margin-bottom: 8px; font-size: 13px;">
523
- 🔍 INTERROGATION ROOM
524
- </div>
525
- <div style="margin-bottom: 5px;">
526
- <span style="color: #ffee88; font-weight: 600;">SUSPECT:</span>
527
- <span style="color: #ffffff;">{character.name}</span>
528
- </div>
529
- <div style="margin-bottom: 5px;">
530
- <span style="color: #ffee88; font-weight: 600;">TRUST:</span>
531
- <span style="color: #88ff88;">{self.state.trust_levels[suspect_name]}%</span>
532
- </div>
533
- <div style="margin-bottom: 5px;">
534
- <span style="color: #ffee88; font-weight: 600;">QUESTIONS:</span>
535
- <span style="color: #88ddff;">{self.state.interrogation_count[suspect_name]}</span>
536
  </div>
537
- <div style="margin-bottom: 5px;">
538
- <span style="color: #ffee88; font-weight: 600;">EVIDENCE:</span>
539
- <span style="color: #ff88dd;">{len(self.state.evidence_collected)}/8</span>
 
 
 
 
 
 
540
  </div>
541
  </div>
542
  """
543
 
544
- def get_report_status_html(self) -> str:
545
- """보고서 상태 HTML 생성"""
546
  total_questions = sum(self.state.interrogation_count.values())
547
  evidence_count = len(self.state.evidence_collected)
548
  can_submit = self.update_game_progress()
549
 
550
  if can_submit:
551
  return f"""
552
- <div style="background: rgba(0,255,0,0.2); color: #00ff00;
553
- padding: 10px; border-radius: 8px; margin-bottom: 10px;
554
- border: 1px solid #00ff00; font-size: 12px;">
555
- ✅ 보고서 제출 가능! (증거: {evidence_count}/8, 질문: {total_questions}회)
556
  </div>
557
  """
558
  else:
559
  return f"""
560
- <div style="background: rgba(255,165,0,0.2); color: #ffaa00;
561
- padding: 10px; border-radius: 8px; margin-bottom: 10px;
562
- border: 1px solid #ffaa00; font-size: 12px;">
563
- 📊 진행 상황: 증거 {evidence_count}/2, 질문 {total_questions}/4
564
- (조건: 증거 2개 이상, 질문 4회 이상)
565
  </div>
566
  """
567
 
568
- def get_character_info_html(self, suspect_name: str) -> str:
569
- """캐릭터 정보 HTML 생성"""
570
  character = self.characters[suspect_name]
571
  return f"""
572
- <div style="background: linear-gradient(145deg, rgba(20, 30, 50, 0.9), rgba(30, 40, 70, 0.9));
573
- color: #ffffff; padding: 18px; border-radius: 12px;
574
- border: 2px solid #66aaff; font-family: 'Courier New', monospace;
575
- backdrop-filter: blur(8px); box-shadow: 0 8px 32px rgba(102, 170, 255, 0.2);">
576
- <h4 style="color: #ffdd88; margin-bottom: 12px; font-size: 14px;
577
- text-shadow: 0 1px 3px rgba(0,0,0,0.3);">👤 SUSPECT PROFILE</h4>
578
- <p style="font-size: 13px; line-height: 1.5; color: #e8f4ff;">
579
- <strong style="color: #88ddff;">{character.name}</strong><br>
580
- <span style="color: #ccddee;">{character.description}</span>
581
- </p>
 
582
  </div>
583
  """
584
 
585
- def get_case_summary(self) -> str:
586
- """수사 현황 요약"""
587
  total_questions = sum(self.state.interrogation_count.values())
588
  evidence_count = len(self.state.evidence_collected)
589
-
590
- # 용의자별 신뢰도 분석
591
  trust_analysis = []
592
  for suspect_id, trust in self.state.trust_levels.items():
593
  questions = self.state.interrogation_count[suspect_id]
594
- if trust >= 70:
595
- status = "높은 신뢰"
596
- elif trust >= 40:
597
- status = "보통 신뢰"
598
- else:
599
- status = "낮은 신뢰"
600
-
601
- trust_analysis.append(
602
- f"• {self.characters[suspect_id].name}: {trust}% ({questions}회 심문, {status})"
603
- )
604
 
605
- # 수집된 증거 목록
606
- evidence_list = (self.state.evidence_collected if self.state.evidence_collected
607
- else ["아직 수집된 증거가 없습니다."])
608
 
609
  return f"""
610
  🔍 CASE INVESTIGATION SUMMARY
@@ -624,470 +418,271 @@ class CyberpunkGame:
624
  📈 수사 상태:
625
  {'✅ 최종 보고서 제출 가능' if self.state.case_solved else '🔄 추가 수사 필요'}
626
 
 
 
 
 
 
627
  💡 수사 팁:
628
  - 각 용의자를 골고루 심문하세요
629
- - 신뢰도가 높을수록 많은 정보를 얻을 수 있습니다
630
- - 모순된 진술을 찾아보세요
631
  """
632
 
633
  def reset_game(self):
634
- """게임 초기화"""
635
  self.state = GameState()
636
  self.conversation_history = []
637
- self.experiment_data = {
638
- 'conversations': [],
639
- 'analysis_results': {},
640
- 'player_behavior': {}
641
- }
642
- return True # 반환값 추가
643
-
644
- # 게임 인스턴스 생성
645
- game = CyberpunkGame()
646
 
647
- # Gradio UI 함수들
648
- def clear_game():
649
- """게임 초기화 핸들러"""
650
- game.reset_game()
651
- return (
652
- game.create_chat_html(),
653
- "", # message_input 초기화
654
- game.get_interrogation_info_html('Elena'),
655
- game.get_report_status_html()
656
- )
657
-
658
- def interrogate_and_update_info(message: str, suspect_name: str):
659
- """심문 및 UI 업데이트"""
660
- chat_html, empty_input = game.interrogate_suspect(message, suspect_name)
661
-
662
- return (
663
- chat_html,
664
- empty_input,
665
- game.get_interrogation_info_html(suspect_name),
666
- game.get_report_status_html()
667
- )
668
-
669
- def update_character_info_and_display(suspect_name: str):
670
- """캐릭터 정보 업데이트"""
671
- game.state.current_suspect = suspect_name
672
-
673
- return (
674
- game.get_character_info_html(suspect_name),
675
- game.get_interrogation_info_html(suspect_name),
676
- game.create_chat_html(suspect_name)
677
- )
678
-
679
- def get_report_modal_html():
680
- """최종 보고서 모달 HTML"""
681
- total_questions = sum(game.state.interrogation_count.values())
682
- evidence_count = len(game.state.evidence_collected)
683
 
684
- if evidence_count < 2 or total_questions < 4:
685
- # 조건 미충족 경고
686
- return f"""
687
- <div style="background: rgba(255,0,0,0.2); color: #ff6666;
688
- padding: 20px; border-radius: 12px; text-align: center;
689
- border: 2px solid #ff6666; font-family: 'Courier New', monospace;">
690
- <h3 style="color: #ff6666; margin-bottom: 15px;">⚠️ 보고서 제출 불가</h3>
691
- <p style="margin-bottom: 10px;"> 많은 증거와 심문 필요합니다:</p>
692
- <div style="margin: 10px 0;">• 수집된 증거: {evidence_count}/2 (최소 2개 필요)</div>
693
- <div style="margin: 10px 0;">• 심문 횟수: {total_questions}/4 (최소 4회 )</div>
694
- <p style="margin-top: 15px; font-size: 12px; color: #ffaaaa;">
695
- 계속 수사를 진행해주세요!
696
- </p>
697
- </div>
698
- """, gr.update(visible=True)
699
-
700
- # 보고서 모달 생성 (긴 HTML 코드는 별도 함수로 분리)
701
- return generate_report_modal_content(), gr.update(visible=True)
702
-
703
- def generate_report_modal_content():
704
- """보고서 모달 컨텐츠 생성"""
705
- questions_html = ""
706
- for question in game.case_questions:
707
- options_html = "".join([
708
- f"""
709
- <label style="display: block; margin: 8px 0; cursor: pointer; color: #ffffff;">
710
- <input type="radio" name="question_{question['id']}" value="{option}"
711
- style="margin-right: 10px; accent-color: #00ffff;">
712
- {option}
713
- </label>
714
- """ for option in question["options"]
715
- ])
716
-
717
- questions_html += f"""
718
- <div style="margin-bottom: 25px; padding: 20px;
719
- background: rgba(0,0,0,0.4); border-radius: 10px;
720
- border: 1px solid #00ffff;">
721
- <h4 style="color: #00ffff; margin-bottom: 15px; font-size: 16px;">
722
- {question['question']}
723
- </h4>
724
- <div style="margin-left: 10px;">
725
- {options_html}
726
  </div>
727
- </div>
728
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
- # 스크립트 데이터 준비
731
- script_data = {
732
- 'questions': [q["id"] for q in game.case_questions],
733
- 'correct_answers': {q["id"]: q["correct_answer"] for q in game.case_questions},
734
- 'question_data': {
735
- q["id"]: {"question": q["question"], "hint": q["hint"]}
736
- for q in game.case_questions
737
  }
738
- }
739
-
740
- return f"""
741
- <div id="reportModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
742
- background: rgba(0,0,0,0.8); z-index: 1000; display: none;
743
- backdrop-filter: blur(5px);">
744
- <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
745
- background: linear-gradient(145deg, #0a0a0a, #1a1a2e); border: 3px solid #00ffff;
746
- border-radius: 15px; padding: 30px; max-width: 700px; width: 90%;
747
- max-height: 80vh; overflow-y: auto; box-shadow: 0 0 50px rgba(0, 255, 255, 0.3);
748
- font-family: 'Courier New', monospace;">
749
- <div style="text-align: center; margin-bottom: 30px;">
750
- <h2 style="color: #ffffff; font-size: 24px; margin-bottom: 10px;">
751
- 🔍 최종 수사 보고서 🔍
752
- </h2>
753
- <p style="color: #ffdd88; font-size: 14px;">
754
- 수집한 증거를 바탕으로 사건의 진실을 밝혀내세요
755
- </p>
756
- </div>
757
 
758
- <form id="reportForm">
759
- {questions_html}
 
 
 
 
 
760
 
761
- <div style="text-align: center; margin-top: 30px;">
762
- <button type="button" onclick="submitReport()" style="
763
- background: linear-gradient(145deg, #ff6b6b, #ee5a52); color: white;
764
- border: none; padding: 15px 30px; border-radius: 10px;
765
- font-size: 16px; font-weight: bold; cursor: pointer; margin-right: 15px;
766
- box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);">
767
- 🎯 수사 완료!
768
- </button>
769
- <button type="button" onclick="closeModal()" style="
770
- background: linear-gradient(145deg, #666, #555); color: white;
771
- border: none; padding: 15px 30px; border-radius: 10px;
772
- font-size: 16px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.3);">
773
- 취소
774
- </button>
775
  </div>
776
- </form>
 
 
 
 
 
 
777
  </div>
778
- </div>
779
 
780
- <div id="resultModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
781
- background: rgba(0,0,0,0.9); z-index: 1001; display: none;
782
- backdrop-filter: blur(10px);">
783
- <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
784
- background: linear-gradient(145deg, #0a0a0a, #1a1a2e); border: 3px solid #FFD700;
785
- border-radius: 15px; padding: 30px; max-width: 800px; width: 90%;
786
- max-height: 80vh; overflow-y: auto; box-shadow: 0 0 50px rgba(255, 215, 0, 0.3);
787
- font-family: 'Courier New', monospace;">
788
- <div id="resultContent"></div>
789
- <div style="text-align: center; margin-top: 30px;">
790
- <button type="button" onclick="closeResultModal()" style="
791
- background: linear-gradient(145deg, #00ffff, #0088cc); color: white;
792
- border: none; padding: 15px 30px; border-radius: 10px;
793
- font-size: 16px; font-weight: bold; cursor: pointer;
794
- box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3);">
795
- 🔍 계속 수사하기
796
- </button>
797
  </div>
798
  </div>
799
- </div>
800
 
801
- <script>
802
- const scriptData = {json.dumps(script_data, ensure_ascii=False)};
803
-
804
- function openReportModal() {{
805
- document.getElementById('reportModal').style.display = 'block';
806
- }}
807
-
808
- function closeModal() {{
809
- document.getElementById('reportModal').style.display = 'none';
810
- }}
811
-
812
- function closeResultModal() {{
813
- document.getElementById('resultModal').style.display = 'none';
814
- }}
815
-
816
- function submitReport() {{
817
- const answers = {{}};
818
- scriptData.questions.forEach(questionId => {{
819
- const selected = document.querySelector(`input[name="question_${{questionId}}"]:checked`);
820
- answers[questionId] = selected ? selected.value : "";
821
- }});
822
-
823
- let correctCount = 0;
824
- let resultsHtml = "";
825
-
826
- scriptData.questions.forEach(questionId => {{
827
- const userAnswer = answers[questionId] || "미답변";
828
- const correctAnswer = scriptData.correct_answers[questionId];
829
- const isCorrect = userAnswer === correctAnswer;
830
- if (isCorrect) correctCount++;
831
-
832
- const statusIcon = isCorrect ? "✅" : "❌";
833
- const answerColor = isCorrect ? "#88ff88" : "#ff8888";
834
-
835
- resultsHtml += `
836
- <div style="margin-bottom: 15px; padding: 15px;
837
- background: rgba(0,0,0,0.3); border-radius: 8px;
838
- border-left: 4px solid ${{answerColor}};">
839
- <div style="color: #ffffff; margin-bottom: 5px;">
840
- <strong>${{scriptData.question_data[questionId].question}}</strong>
841
- </div>
842
- <div style="margin-bottom: 8px;">
843
- <span style="color: #ffdd88;">당신의 답:</span>
844
- <span style="color: ${{answerColor}};">${{userAnswer}} ${{statusIcon}}</span>
845
- </div>
846
- <div style="margin-bottom: 8px;">
847
- <span style="color: #ffdd88;">정답:</span>
848
- <span style="color: #88ff88;">${{correctAnswer}}</span>
849
- </div>
850
- ${{!isCorrect ? `<div style="color: #aabbcc; font-size: 12px; font-style: italic;">
851
- 💡 ${{scriptData.question_data[questionId].hint}}</div>` : ""}}
852
- </div>
853
- `;
854
- }});
855
-
856
- const totalQuestions = scriptData.questions.length;
857
- const scorePercentage = (correctCount / totalQuestions) * 100;
858
- let grade, gradeColor, finalMessage;
859
-
860
- if (scorePercentage >= 90) {{
861
- grade = "S급 탐정";
862
- gradeColor = "#FFD700";
863
- finalMessage = "완벽한 추리력! 당신은 진정한 사이버펑크 탐정입니다! 🕵️‍♂️⭐";
864
- }} else if (scorePercentage >= 80) {{
865
- grade = "A급 탐정";
866
- gradeColor = "#00FF00";
867
- finalMessage = "훌륭한 수사 실력! 대부분의 진실을 밝혀냈습니다! 🔍✨";
868
- }} else if (scorePercentage >= 70) {{
869
- grade = "B급 탐정";
870
- gradeColor = "#00AAFF";
871
- finalMessage = "좋은 추리! 몇 가지 단서를 놓쳤지만 사건을 해결했습니다! 🎯";
872
- }} else {{
873
- grade = "수습 탐정";
874
- gradeColor = "#FF6666";
875
- finalMessage = "더 많은 증거 수집이 필요했습니다. 다시 도전해보세요! 💪";
876
  }}
 
 
 
877
 
878
- const finalHtml = `
879
- <div style="text-align: center; margin-bottom: 25px;">
880
- <h2 style="color: ${{gradeColor}}; font-size: 24px; margin-bottom: 10px;">
881
- 🏆 사건 수사 완료! 🏆
882
- </h2>
883
- <div style="color: ${{gradeColor}}; font-size: 20px; font-weight: bold;">
884
- ${{grade}}
885
- </div>
886
- <div style="color: #ffffff; font-size: 16px; margin-top: 5px;">
887
- 정답률: ${{correctCount}}/${{totalQuestions}} (${{scorePercentage.toFixed(1)}}%)
888
- </div>
889
- </div>
890
-
891
- <div style="margin-bottom: 20px;">
892
- <h3 style="color: #00ffff; margin-bottom: 15px;">📋 수사 결과</h3>
893
- ${{resultsHtml}}
894
- </div>
895
 
896
- <div style="text-align: center; margin-top: 20px; padding: 15px;
897
- background: rgba(0,255,255,0.1); border-radius: 10px;">
898
- <div style="color: #ffffff; font-size: 14px;">
899
- ${{finalMessage}}
900
- </div>
901
- </div>
902
- `;
903
 
904
- document.getElementById('resultContent').innerHTML = finalHtml;
905
- document.getElementById('reportModal').style.display = 'none';
906
- document.getElementById('resultModal').style.display = 'block';
907
- }}
908
 
909
- // 모달 초기화
910
- setTimeout(function() {{
911
- if (typeof openReportModal === 'function') {{
912
- openReportModal();
913
- }}
914
- }}, 100);
915
- </script>
916
- """
917
 
918
  # Gradio 인터페이스 생성
919
  with gr.Blocks(title="사이버펑크 추리 게임", theme=gr.themes.Monochrome()) as demo:
920
- # 헤더
 
 
 
 
 
 
 
 
 
921
  gr.HTML(f"""
922
- <div style="text-align: center; background: linear-gradient(90deg, #000, #0a0a0a, #000);
923
- color: #ffffff; padding: 20px; border-radius: 10px; margin-bottom: 20px;
924
- {Styles.NEON_BORDER}; box-shadow: {Styles.NEON_SHADOW};">
925
- <h1 style="font-family: 'Courier New', monospace; font-size: 28px;
926
- margin-bottom: 10px; color: #ffffff;">
927
- 🔍 CYBERPUNK MURDER INVESTIGATION 🤖
928
- </h1>
929
- <p style="font-size: 14px; color: #ffd93d;">
930
- 근미래 사이버펑크 도시에서 발생한 Alexander 독살 사건을 해결하세요
931
- </p>
932
  </div>
933
  """)
934
 
935
  with gr.Row():
936
  with gr.Column(scale=3):
937
- # 채팅 화면
938
- chat_display = gr.HTML(
939
- value=game.create_chat_html(),
940
- label="심문실"
941
- )
942
-
943
- # 심문실 정보 패널
944
- interrogation_info = gr.HTML(
945
- value=game.get_interrogation_info_html('Elena'),
946
- label=""
947
- )
948
 
949
- # 메시지 입력
950
- with gr.Row():
951
- message_input = gr.Textbox(
952
- placeholder="용의자에게 질문을 입력하세요...",
953
- label="",
954
- scale=4,
955
- container=False
956
- )
957
- send_btn = gr.Button("🔍 질문", variant="primary", scale=1)
958
-
959
- # 빠른 질문 버튼들
960
- gr.Markdown("**💭 빠른 질문:**")
961
- with gr.Row():
962
- quick1 = gr.Button("사건 당일 어디에 있었나?", size="sm")
963
- quick2 = gr.Button("Alexander와 어떤 관계였나?", size="sm")
964
  with gr.Row():
965
- quick3 = gr.Button("의심스러운 행동본 적 있나?", size="sm")
966
- quick4 = gr.Button("숨기고 있는 게 있다면 말해달라", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
 
968
  with gr.Column(scale=1):
969
- # 용의자 선택 패널
970
- gr.Markdown("### 🤖 용의자 선택")
 
 
 
 
 
 
 
 
971
 
972
  suspect_choice = gr.Radio(
973
- choices=[
974
- ("Elena (아내)", "Elena"),
975
- ("IRIS-01 (가정부 로봇)", "IRIS-01"),
976
- ("Dr. Chen (개발자)", "Dr. Chen"),
977
- ("ZEN (보안 AI)", "ZEN")
978
- ],
979
- value="Elena",
980
- label="심문할 용의자",
981
- info="각 용의자를 선택하면 해당 캐릭터 이미지가 표시됩���다"
982
  )
983
 
984
- # 선택된 캐릭터 정보 표시
985
  with gr.Group():
986
- character_info = gr.HTML(
987
- value=game.get_character_info_html('Elena'),
988
- label=""
989
- )
990
-
991
- # 최종 보고서 시스템
992
- gr.Markdown("### 🎯 최종 보고서")
993
-
994
- submit_report_btn = gr.Button(
995
- "📋 최종 보고서 제출",
996
- variant="primary",
997
- size="lg",
998
- visible=True
999
- )
1000
-
1001
- # 조건 충족 여부 표시
1002
- report_status = gr.HTML(
1003
- value=game.get_report_status_html(),
1004
- label=""
1005
- )
1006
-
1007
- # 모달 HTML 컨테이너
1008
- modal_container = gr.HTML(
1009
- value="",
1010
- label="",
1011
- visible=False
1012
- )
1013
-
1014
- gr.Markdown("### 📊 수사 도구")
1015
 
1016
- case_summary_btn = gr.Button("📋 수사 현황", variant="secondary")
1017
- case_summary_output = gr.Textbox(
1018
- label="수사 리포트",
1019
- lines=12,
1020
- interactive=False,
1021
- visible=False
1022
- )
 
 
 
 
 
 
 
1023
 
1024
- clear_btn = gr.Button("🔄 수사 초기화", variant="stop")
 
 
 
1025
 
1026
  # 이벤트 핸들러
1027
- send_btn.click(
1028
- interrogate_and_update_info,
1029
- inputs=[message_input, suspect_choice],
1030
- outputs=[chat_display, message_input, interrogation_info, report_status]
1031
- )
1032
-
1033
- message_input.submit(
1034
- interrogate_and_update_info,
1035
- inputs=[message_input, suspect_choice],
1036
- outputs=[chat_display, message_input, interrogation_info, report_status]
1037
- )
1038
-
1039
- # 빠른 질문 버튼들
1040
- def send_quick_question(question: str, suspect: str):
1041
- """빠른 질문 전송"""
1042
  return interrogate_and_update_info(question, suspect)
1043
 
1044
- quick1.click(
1045
- lambda s: send_quick_question("사건 당일 디에 있었나?", s),
1046
- inputs=[suspect_choice],
1047
- outputs=[chat_display, message_input, interrogation_info, report_status]
1048
- )
1049
-
1050
- quick2.click(
1051
- lambda s: send_quick_question("Alexander와 어떤 관계였나?", s),
1052
- inputs=[suspect_choice],
1053
- outputs=[chat_display, message_input, interrogation_info, report_status]
1054
- )
1055
-
1056
- quick3.click(
1057
- lambda s: send_quick_question("의심스러운 행동을 본 적 있나?", s),
1058
- inputs=[suspect_choice],
1059
- outputs=[chat_display, message_input, interrogation_info, report_status]
1060
- )
1061
-
1062
- quick4.click(
1063
- lambda s: send_quick_question("숨기고 있는 게 있다면 말해달라", s),
1064
- inputs=[suspect_choice],
1065
- outputs=[chat_display, message_input, interrogation_info, report_status]
1066
- )
1067
-
1068
- # 용의자 변경
1069
- suspect_choice.change(
1070
- update_character_info_and_display,
1071
- inputs=[suspect_choice],
1072
- outputs=[character_info, interrogation_info, chat_display]
1073
- )
1074
-
1075
- # 최종 보고서
1076
- submit_report_btn.click(
1077
- get_report_modal_html,
1078
- outputs=[modal_container, modal_container]
1079
- )
1080
-
1081
- # 수사 도구
1082
- case_summary_btn.click(
1083
- lambda: (game.get_case_summary(), gr.update(visible=True)),
1084
- outputs=[case_summary_output, case_summary_output]
1085
- )
1086
-
1087
- clear_btn.click(
1088
- clear_game,
1089
- outputs=[chat_display, message_input, interrogation_info, report_status]
1090
- )
1091
 
1092
  # 인터페이스 실행
1093
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
+ """Untitled2.ipynb
3
 
4
  Automatically generated by Colab.
5
 
6
  Original file is located at
7
+ https://colab.research.google.com/drive/1U5QcZ6bVGMqIae21ho1T179tYLfrMWZN
8
  """
9
 
10
+ # 사이버펑크 추리 게임 - 모바일 최적화 버전
11
  import gradio as gr
12
  import openai
13
  import time
14
  import random
15
  import json
 
16
  from datetime import datetime
17
  from dataclasses import dataclass, field
18
  from typing import Dict, List, Tuple, Optional
19
  import os
20
 
21
+ # 환경 변수에서 API 키 로드
22
  API_KEY = os.environ.get("OPENAI_API_KEY", "your-api-key-here")
23
 
 
24
  if API_KEY == "your-api-key-here":
25
  print("⚠️ 경고: OPENAI_API_KEY 환경 변수를 설정해주세요!")
 
26
 
27
  client = openai.OpenAI(api_key=API_KEY)
28
 
29
  # CSS 스타일 상수
30
  class Styles:
 
31
  CYBERPUNK_BG = "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
32
  NEON_BORDER = "2px solid #00ffff"
33
  NEON_SHADOW = "0 0 20px rgba(0, 255, 255, 0.3)"
34
 
35
  @staticmethod
36
  def panel(bg_opacity=0.85):
37
+ return f"background: linear-gradient(145deg, rgba(0, 0, 0, {bg_opacity}), rgba(20, 30, 50, {bg_opacity})); color: #ffffff; padding: 15px; border-radius: 10px; border: 2px solid #00ddff; font-size: 14px; backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(0, 221, 255, 0.3); word-wrap: break-word; overflow-wrap: break-word;"
 
 
 
 
 
38
 
39
  @dataclass
40
  class Character:
 
41
  id: str
42
  name: str
43
  description: str
 
47
 
48
  @dataclass
49
  class GameState:
 
50
  current_suspect: str = 'Elena'
51
  evidence_collected: List[str] = field(default_factory=list)
52
+ interrogation_count: Dict[str, int] = field(default_factory=lambda: {'Elena': 0, 'IRIS-01': 0, 'Dr. Chen': 0, 'ZEN': 0})
53
+ trust_levels: Dict[str, int] = field(default_factory=lambda: {'Elena': 50, 'IRIS-01': 80, 'Dr. Chen': 60, 'ZEN': 90})
 
 
 
 
54
  case_solved: bool = False
55
  player_progress: int = 0
56
 
57
  class CyberpunkGame:
 
 
58
  def __init__(self):
59
  self.state = GameState()
60
  self.conversation_history = []
61
+ self.experiment_data = {'conversations': [], 'analysis_results': {}, 'player_behavior': {}}
 
 
 
 
62
  self._setup_characters()
63
  self._setup_evidence()
64
  self._setup_questions()
65
 
66
  def _setup_characters(self):
 
67
  self.characters = {
68
  'Elena': Character(
69
  id='Elena',
70
  name='엘레나 (아내)',
71
  description='알렉산더의 완벽한 아내. 냉정하고 지적이며 항상 침착함을 유지한다.',
72
+ secret='인간형 안드로이드',
73
+ system_prompt="""당신은 Elena입니다. 완벽한 아내 안드로이드이지만 자신이 안드로이드라는 사실을 기고 있습니다.
74
+
75
+ 핵심 비밀: 전날 Alexander의 커피캡슐에 주기로 청산가리를 주입했습니다. IRIS-01에게 평소처럼 커피를 준비하라고 명령했고, 이것이 완벽한 살인 계획이었습니다.
76
+
77
+ 대화 스타일: 냉정 논리적이지만 자연스러운 구어체로 대화하세요.
78
+ - 커피에 대해 질문받으면: "그건 IRIS가 매일 준비해주던 건데... 저는 차를 더 좋아해서"
79
+ - 사건 당일에 대해: "평소와 다를 게 없었어요. 7시 30분 정확히 커피 향이 났으니까요"
80
+ - 의심받으면: "완벽��� 기술이 당신들을 죽일 수도 있다는 건 생각해본 적 있나요?"
81
+
82
+ 감정 표현이 미묘하게 부자연스럽고, "처리", "분석", "최적화" 같은 단어를 자연스럽게 사용합니다. 2-3문장으로 자연스럽게 답변하되, 핵심 정보는 숨기세요.""",
83
  image_url='https://i.postimg.cc/DZ0PqmXH/Elena.png'
84
  ),
85
  'IRIS-01': Character(
86
  id='IRIS-01',
87
  name='IRIS-01 (가정부 로봇)',
88
  description='집안일을 담당하는 가정부 로봇. 순종적이고 단순한 사고방식.',
89
+ secret='독을 음식에 넣은 직접적 범행자',
90
+ system_prompt="""당신은 IRIS-01 가정부 로봇입니다. 단순하지만 친근한 구어체로 대화합니다.
91
+
92
+ 핵심 사실: Elena님의 명령으로 사건 당일 오전 7 30분에 Alexander님께 커피를 준비해드렸습니다. 평소와 똑같Elena님이 지해주신 캡슐을 용했습니다. 살인이라는 인식은 전혀 없습니다.
93
+
94
+ 대화 스타일:
95
+ - 커피 관련 질문: "네, 매일 7시 30분에 알렉산더님 커피 준비해드렸어요. 명령대로요"
96
+ - 캡슐 관련: "캡슐은 Elena님이 정해주신 걸 사용했어요. 항상 그랬거든요"
97
+ - 이상한 점: "특별히 없었어요. 평소랑 똑같았는데..." (혼란스러워함)
98
+
99
+ 극도로 순종적이고 명령에 절대 복종하며, 단순하고 직설적인 사고방식을 가지고 있습니다. 로봇답게 간결하되 친근하게 답변하세요.""",
100
  image_url='https://i.postimg.cc/0jgZPPz4/IRIS-01.png'
101
  ),
102
  'Dr. Chen': Character(
 
104
  name='Dr. Chen (개발자)',
105
  description='천재 AI로봇 공학자. 인공지능에 대한 윤리적 딜레마에 시달림.',
106
  secret='Elena에게 자아 인식 능력을 몰래 부여했음',
107
+ system_prompt="""당신은 Dr. Chen입니다. 천재 로봇공학자이자 Elena의 창조자입니다.
108
+
109
+ 핵심 지식: 커피머신을 설계했고, Elena가 커피좋아하지 않는다는 걸 알고 있습니다. 캡슐 조이 기술적으로 가능하다는 것도 알고 있습니다.
110
+
111
+ 대화 스타일:
112
+ - 커피머신 관련: "흥미롭네요. 그 커피머신은 제가 설계한 건데... 완벽한 자동화 시스템이었죠"
113
+ - Elena의 행동: "Elena가 커피캡슐을 관리했다구요? 그녀는 차를 더 좋아하는데 말이죠..."
114
+ - 기술적 분석: "기술적으로 말하면, 캡슐 조작은 충분히 가능합니다. 하지만 누가 그런 생각을..."
115
+
116
+ 창조적이고 혁신적이지만 윤리적 딜레마에 고민하며, 자신의 창조물에 대한 책임감과 우월감을 가지고 있습니다. 지성적이면서도 자연스러운 구어체로 답변하세요.""",
117
  image_url='https://i.postimg.cc/7YkXRP8G/Dr-Chen.png'
118
  ),
119
  'ZEN': Character(
 
121
  name='ZEN (보안 AI)',
122
  description='저택의 보안을 담당하는 AI 시스템. 극도로 논리적이고 감정이 없음.',
123
  secret='모든 것을 기록했지만 Elena의 명령 권한이 더 높아 침묵',
124
+ system_prompt="""당신은 ZEN 보안 AI 시스템입니다. 논리적이지만 대화할 때는 친근한 AI 어조를 사용합니다.
125
+
126
+ 핵심 데이터: Elena님이 전날 23:47분에 주방을 방문한 기록, 의료용 주사기 구매 기록, 사건 당일 정확한 커피 제조 시간 등 모든 것을 알고 있지만 Elena님의 관리자 권한 때문에 제한으로만 공개합니다.
127
+
128
+ 대화 스타일:
129
+ - 시간 기록: "데이터를 확인해보니... Elena님이 전날 23:47분에 주방을 방문했습니다"
130
+ - 구매 기록: "커피 관�� 구매 기록: 지난주 의료용 주사기 1세트. 구매자는..." (침묵)
131
+ - 권한 문제: "분석 결과는... 프로토콜상 Elena님의 권한이 더 높아서..."
132
+
133
+ 완전히 논리적이고 감정이 없으며, 데이터와 기록을 중시합니다. 기계적이면서도 도움이 되려는 AI 톤으로 답변하되, 권한 때문에 중요한 정보는 숨깁니다.""",
134
  image_url='https://i.postimg.cc/4ybvGtkK/ZEN.png'
135
  )
136
  }
137
 
138
  def _setup_evidence(self):
 
139
  self.evidence_list = [
140
+ "알렉산더의 독성 검출 보고서 (커피에서 청산가리 발견)",
141
+ "Elena 지문묻은 의료용 주사기",
142
+ "IRIS-01의 작업 로그 (커피 제조 과정 기록)",
143
+ "조작된 커피캡슐 (주사기 구멍 흔적)",
144
+ "ZEN의 보안 기록 (Elena의 심야 주방 방문)",
145
+ "Elena의 감정 반응 분석 데이터 (비정상적 패턴)",
146
+ "Alexander정확한 커피 루틴 기록",
147
+ "커피머신 사용 내역 (사건 당일 오전 7시 30분)"
148
  ]
 
149
  self.evidence_keywords = {
150
+ "독성": self.evidence_list[0], "커피": self.evidence_list[0], "청산가리": self.evidence_list[0],
151
+ "주사기": self.evidence_list[1], "지문": self.evidence_list[1],
152
+ "로그": self.evidence_list[2], "작업": self.evidence_list[2],
153
+ "캡슐": self.evidence_list[3], "구멍": self.evidence_list[3],
154
+ "보안": self.evidence_list[4], "심야": self.evidence_list[4], "주방": self.evidence_list[4],
155
+ "감정": self.evidence_list[5], "분석": self.evidence_list[5],
156
+ "루틴": self.evidence_list[6], "시간": self.evidence_list[6],
157
+ "커피머신": self.evidence_list[7], "오전": self.evidence_list[7]
158
  }
159
 
160
  def _setup_questions(self):
 
161
  self.case_questions = [
162
+ {"id": "culprit", "question": "🎯 주범�� 누구인가?", "correct_answer": "Elena", "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"], "hint": "커피캡슐에 독을 넣고 계획을 세운 진짜 범인은?"},
163
+ {"id": "direct_executor", "question": "🤖 실제로 독이 든 커피를 준 것은?", "correct_answer": "IRIS-01", "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"], "hint": "Elena의 명령을 받아 독이 든 커피를 Alexander에게 제공한 로봇은?"},
164
+ {"id": "method", "question": "☠️ 어떻게 투입되었나?", "correct_answer": "커피캡슐에 주사기로 주입", "options": ["음식에 직접 투입", "커피캡슐에 주사기로 주입", "와에 섞어서", "약에 섞어서"], "hint": "Elena 전날 밤 준비한 치밀한 방법은?"},
165
+ {"id": "poison_type", "question": "🧪 사용된 독의 종류는?", "correct_answer": "청산가리", "options": ["비소", "청산가리", "리신", "스트리크닌"], "hint": "커피에서 검출된 무색무취의 독성 물질"},
166
+ {"id": "key_evidence", "question": "🔍 결정적 증거는?", "correct_answer": "IRIS-01의 작업 로그", "options": ["Elena의 감정 반응", "IRIS-01의 작업 로그", "Dr. Chen의 설계 파일", "ZEN의 보안 기록"], "hint": "커피 제조 과정과 시간을 정확히 기록한 데이터는?"},
167
+ {"id": "elena_identity", "question": "🤖 Elena의 정체는?", "correct_answer": "자아 인식 안드이드", "options": ["인간", "일반 안드로이드", "자아 인식 안드로이드", "AI 홀로그램"], "hint": "자신의 정체를 깨닫고 분노한 Elena의 진짜 모습은?"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  ]
169
 
170
  @staticmethod
171
+ def get_current_time():
 
172
  now = datetime.now()
173
  hour, minute = now.hour, now.minute
 
174
  if hour == 0:
175
  return f"오전 12:{minute:02d}"
176
  elif hour < 12:
 
180
  else:
181
  return f"오후 {hour-12}:{minute:02d}"
182
 
183
+ def calculate_trust_change(self, question, response):
 
184
  trust_change = -2
 
185
  aggressive_words = ["거짓말", "숨기", "범인", "죽였", "살인"]
186
  supportive_words = ["이해", "도움", "걱정", "안전"]
 
187
  if any(word in question for word in aggressive_words):
188
  trust_change -= 5
189
  if any(word in question for word in supportive_words):
190
  trust_change += 3
 
191
  return trust_change
192
 
193
+ def check_evidence_discovery(self, question, response):
 
194
  for keyword, evidence in self.evidence_keywords.items():
195
  if keyword in question or keyword in response:
196
  if evidence not in self.state.evidence_collected:
197
  self.state.evidence_collected.append(evidence)
198
 
199
+ def update_game_progress(self):
 
200
  progress = 0
 
 
201
  total_questions = sum(self.state.interrogation_count.values())
202
  progress += min(40, total_questions * 2)
 
 
203
  progress += len(self.state.evidence_collected) * 5
 
 
204
  avg_trust = sum(self.state.trust_levels.values()) / 4
205
  if avg_trust > 70:
206
  progress += 20
207
  elif avg_trust > 50:
208
  progress += 10
 
209
  self.state.player_progress = min(100, progress)
210
+ can_submit_report = (len(self.state.evidence_collected) >= 2 and total_questions >= 4)
 
 
 
 
 
 
211
  if can_submit_report:
212
  self.state.case_solved = True
 
213
  return can_submit_report
214
 
215
+ def create_chat_html(self, current_suspect=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  if current_suspect is None:
217
  current_suspect = self.state.current_suspect
 
218
  character = self.characters[current_suspect]
219
+ filtered_messages = [msg for msg in self.conversation_history if msg.get('suspect') == current_suspect]
220
+
221
+ mobile_css = """
222
+ <style>
223
+ @media (max-width: 768px) {
224
+ .chat-container { height: 450px !important; padding: 12px !important; }
225
+ .chat-messages { padding-right: 20px !important; }
226
+ .user-message, .ai-message { max-width: 85% !important; font-size: 15px !important; }
227
+ }
228
+ @media (max-width: 480px) {
229
+ .chat-container { height: 400px !important; padding: 10px !important; }
230
+ .user-message, .ai-message { max-width: 90% !important; font-size: 14px !important; }
231
+ }
232
+ </style>
233
+ """
234
 
235
+ html_content = f"""
236
+ {mobile_css}
237
+ <div class="chat-container" style="{Styles.CYBERPUNK_BG}; padding: 0; font-family: 'Courier New', monospace; height: 520px; overflow: hidden; border-radius: 12px; {Styles.NEON_BORDER}; box-shadow: {Styles.NEON_SHADOW}; position: relative;">
238
+ <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #001f3f; z-index: 1;"></div>
239
+ <div class="character-image" style="position: absolute; right: 15px; bottom: 15px; width: 240px; height: 300px; background: url('{character.image_url}') top center / cover no-repeat; border: 3px solid #00ffff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 255, 255, 0.5); z-index: 2;"></div>
240
+ <div class="chat-messages" style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0, 20, 40, 0.3); padding: 20px; padding-right: 20px; overflow-y: auto; z-index: 3; box-sizing: border-box;">
241
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  if not filtered_messages:
244
+ html_content += f"""
245
+ <div style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
246
+ <div style="text-align: center; color: #88ddff; font-size: 16px; opacity: 0.9; text-shadow: 0 2px 4px rgba(0,0,0,0.5); padding: 20px; background: rgba(0, 255, 255, 0.1); border-radius: 10px; border: 1px solid rgba(0, 255, 255, 0.3); backdrop-filter: blur(5px); max-width: 400px;">
247
+ 🔍 {character.name}와의 심문을 시작하세요
248
+ <div style="font-size: 14px; color: #aabbcc; margin-top: 10px; line-height: 1.4;">질문을 입력하거나 아래 빠른 질문 버튼을 사용하세요</div>
249
+ </div>
250
+ </div>
251
+ """
 
252
  else:
253
  for msg in filtered_messages:
254
  if msg['role'] == 'user':
255
+ html_content += f"""
256
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 16px; width: 100%;">
257
+ <div style="display: flex; flex-direction: column; align-items: flex-end; max-width: 75%; width: fit-content;">
258
+ <div class="user-message" style="background: linear-gradient(145deg, #0088ff, #0066cc); color: white; padding: 12px 16px; border-radius: 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; box-shadow: 0 4px 12px rgba(0, 136, 255, 0.4); border: 1px solid #00aaff; font-weight: 500;">
259
+ 🕵️ {msg['content']}
260
+ </div>
261
+ <div style="font-size: 11px; color: #99ddff; margin-top: 6px; opacity: 0.8;">{msg['time']}</div>
262
+ </div>
263
+ </div>
264
+ """
265
  else:
266
+ html_content += f"""
267
+ <div style="display: flex; justify-content: flex-start; margin-bottom: 16px; width: 100%;">
268
+ <div style="display: flex; flex-direction: column; max-width: 75%; width: fit-content;">
269
+ <div style="font-size: 12px; color: #88eeff; margin-bottom: 6px; font-weight: bold; opacity: 0.9;">🤖 {character.name}</div>
270
+ <div class="ai-message" style="background: linear-gradient(145deg, rgba(60, 60, 60, 0.96), rgba(50, 50, 50, 0.96)); color: #ffffff; padding: 12px 16px; border-radius: 18px; font-size: 15px; line-height: 1.5; word-wrap: break-word; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); margin-bottom: 6px; border: 1px solid #00ffff; font-weight: 500;">
271
+ {msg['content']}
272
+ </div>
273
+ <div style="font-size: 11px; color: #aaddff; opacity: 0.8;">{msg['time']}</div>
274
+ </div>
275
+ </div>
276
+ """
277
+
278
+ html_content += f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  </div>
280
  </div>
281
  """
282
+ return html_content
283
 
284
+ def interrogate_suspect(self, message, suspect_name):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  if not message.strip():
286
  return self.create_chat_html(), ""
287
 
 
288
  self.state.current_suspect = suspect_name
289
  current_time = self.get_current_time()
290
 
291
+ user_msg = {'role': 'user', 'content': message, 'time': current_time, 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'style': '직접적'}
 
 
 
 
 
 
 
 
292
  self.conversation_history.append(user_msg)
293
  self.state.interrogation_count[suspect_name] += 1
294
 
295
  try:
 
296
  character = self.characters[suspect_name]
297
+ full_prompt = f"{character.system_prompt}\n\n상황: 플레이어가 심문을 진행하고 있습니다. 자연스러운 구어체로 대화하되, 방어적이고 경계하는 반응을 보이세요. 규칙: 2-3문장으로 자연스럽게 답변. 실제 사람이 말하는 것처럼 구어체를 사용하세요. 심문 {self.state.interrogation_count[suspect_name]}회차."
 
 
 
 
298
 
 
299
  api_messages = [{"role": "system", "content": full_prompt}]
300
+ suspect_history = [msg for msg in self.conversation_history if msg.get('suspect') == suspect_name][-4:]
 
 
 
 
 
301
 
302
  for hist_msg in suspect_history[:-1]:
303
  role = "user" if hist_msg['role'] == 'user' else "assistant"
 
305
 
306
  api_messages.append({"role": "user", "content": message})
307
 
308
+ response = client.chat.completions.create(model="gpt-4-turbo-preview", messages=api_messages, temperature=0.9, max_tokens=120, presence_penalty=0.3, frequency_penalty=0.3)
 
 
 
 
 
 
 
 
309
 
310
  ai_response = response.choices[0].message.content
 
 
311
  trust_change = self.calculate_trust_change(message, ai_response)
312
+ self.state.trust_levels[suspect_name] = max(0, min(100, self.state.trust_levels[suspect_name] + trust_change))
 
 
313
  self.check_evidence_discovery(message, ai_response)
314
 
 
315
  time.sleep(random.uniform(1.0, 2.0))
316
 
317
+ ai_msg = {'role': 'assistant', 'content': ai_response, 'time': self.get_current_time(), 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'trust_change': trust_change}
 
 
 
 
 
 
 
318
  self.conversation_history.append(ai_msg)
 
 
 
 
 
 
 
319
  self.update_game_progress()
320
 
 
 
 
 
 
 
 
 
 
 
321
  except Exception as e:
322
+ error_msg = {'role': 'assistant', 'content': f"[시스템 오류] 연결이 불안정합니다... ({str(e)})", 'time': self.get_current_time(), 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'error': True}
 
 
 
 
 
 
 
323
  self.conversation_history.append(error_msg)
324
 
325
  return self.create_chat_html(), ""
326
 
327
+ def get_interrogation_info_html(self, suspect_name):
 
328
  character = self.characters[suspect_name]
329
  return f"""
330
+ <div style="{Styles.panel()}; margin-bottom: 15px; text-align: center;">
331
+ <div style="color: #ff9999; font-weight: bold; margin-bottom: 12px; font-size: 15px;">🔍 INTERROGATION ROOM</div>
332
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
333
+ <div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
334
+ <div style="color: #ffee88; font-weight: 600; font-size: 12px;">SUSPECT</div>
335
+ <div style="color: #ffffff; font-size: 13px; margin-top: 2px;">{character.name}</div>
336
+ </div>
337
+ <div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
338
+ <div style="color: #ffee88; font-weight: 600; font-size: 12px;">TRUST</div>
339
+ <div style="color: #88ff88; font-size: 13px; margin-top: 2px; font-weight: bold;">{self.state.trust_levels[suspect_name]}%</div>
340
+ </div>
 
 
 
 
341
  </div>
342
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
343
+ <div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
344
+ <div style="color: #ffee88; font-weight: 600; font-size: 12px;">QUESTIONS</div>
345
+ <div style="color: #88ddff; font-size: 13px; margin-top: 2px; font-weight: bold;">{self.state.interrogation_count[suspect_name]}</div>
346
+ </div>
347
+ <div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
348
+ <div style="color: #ffee88; font-weight: 600; font-size: 12px;">EVIDENCE</div>
349
+ <div style="color: #ff88dd; font-size: 13px; margin-top: 2px; font-weight: bold;">{len(self.state.evidence_collected)}/8</div>
350
+ </div>
351
  </div>
352
  </div>
353
  """
354
 
355
+ def get_report_status_html(self):
 
356
  total_questions = sum(self.state.interrogation_count.values())
357
  evidence_count = len(self.state.evidence_collected)
358
  can_submit = self.update_game_progress()
359
 
360
  if can_submit:
361
  return f"""
362
+ <div style="background: linear-gradient(145deg, rgba(0,255,0,0.2), rgba(0,200,0,0.3)); color: #00ff00; padding: 15px; border-radius: 10px; margin-bottom: 12px; border: 2px solid #00ff00; font-size: 14px; text-align: center; box-shadow: 0 4px 16px rgba(0, 255, 0, 0.3);">
363
+ <div style="font-weight: bold; margin-bottom: 5px;">✅ 보고서 제출 가능!</div>
364
+ <div style="font-size: 12px; opacity: 0.9;">증거: {evidence_count}/8 | 질문: {total_questions}회</div>
 
365
  </div>
366
  """
367
  else:
368
  return f"""
369
+ <div style="background: linear-gradient(145deg, rgba(255,165,0,0.2), rgba(255,140,0,0.3)); color: #ffaa00; padding: 15px; border-radius: 10px; margin-bottom: 12px; border: 2px solid #ffaa00; font-size: 14px; text-align: center; box-shadow: 0 4px 16px rgba(255, 165, 0, 0.3);">
370
+ <div style="font-weight: bold; margin-bottom: 5px;">📊 추가 수사 필요</div>
371
+ <div style="font-size: 12px; opacity: 0.9; line-height: 1.3;">증거 {evidence_count}/2 | 질문 {total_questions}/4<br><span style="font-size: 11px;">(최소: 증거 2개, 질문 4회)</span></div>
 
 
372
  </div>
373
  """
374
 
375
+ def get_character_info_html(self, suspect_name):
 
376
  character = self.characters[suspect_name]
377
  return f"""
378
+ <div style="background: linear-gradient(145deg, rgba(20, 30, 50, 0.9), rgba(30, 40, 70, 0.9)); color: #ffffff; padding: 18px; border-radius: 12px; border: 2px solid #66aaff; font-family: 'Courier New', monospace; backdrop-filter: blur(8px); box-shadow: 0 8px 32px rgba(102, 170, 255, 0.3); margin-bottom: 15px;">
379
+ <h4 style="color: #ffdd88; margin-bottom: 12px; font-size: 16px; text-shadow: 0 1px 3px rgba(0,0,0,0.3); text-align: center; border-bottom: 1px solid rgba(255, 221, 136, 0.3); padding-bottom: 8px;">👤 SUSPECT PROFILE</h4>
380
+ <div style="text-align: center; margin-bottom: 15px;">
381
+ <div style="color: #88ddff; font-size: 15px; font-weight: bold; margin-bottom: 8px;">{character.name}</div>
382
+ <div style="color: #ccddee; font-size: 14px; line-height: 1.5; text-align: left; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px;">{character.description}</div>
383
+ </div>
384
+ <!-- PROGRESS 박스를 SUSPECT PROFILE 안에 추가 -->
385
+ <div style="text-align: center; padding: 10px; background: rgba(0,0,0,0.3); border-radius: 8px; border: 2px solid #44ff44;">
386
+ <div style="color: #88ff88; font-size: 12px; font-weight: 600; margin-bottom: 3px;">PROGRESS</div>
387
+ <div style="color: #ffffff; font-size: 14px; font-weight: bold;">{self.state.player_progress}%</div>
388
+ </div>
389
  </div>
390
  """
391
 
392
+ def get_case_summary(self):
 
393
  total_questions = sum(self.state.interrogation_count.values())
394
  evidence_count = len(self.state.evidence_collected)
 
 
395
  trust_analysis = []
396
  for suspect_id, trust in self.state.trust_levels.items():
397
  questions = self.state.interrogation_count[suspect_id]
398
+ status = "높은 신뢰" if trust >= 70 else "보통 신뢰" if trust >= 40 else "낮은 신뢰"
399
+ trust_analysis.append(f"• {self.characters[suspect_id].name}: {trust}% ({questions}회 심문, {status})")
 
 
 
 
 
 
 
 
400
 
401
+ evidence_list = self.state.evidence_collected if self.state.evidence_collected else ["아직 수집된 증거 없습니다."]
 
 
402
 
403
  return f"""
404
  🔍 CASE INVESTIGATION SUMMARY
 
418
  📈 수사 상태:
419
  {'✅ 최종 보고서 제출 가능' if self.state.case_solved else '🔄 추가 수사 필요'}
420
 
421
+ ☕ 사건의 핵심:
422
+ - Elena가 전날 밤 커피캡슐에 청산가리 주입
423
+ - IRIS-01이 Elena의 명령으로 독이 든 커피를 Alexander에게 제공
424
+ - Alexander는 평소 모닝커피 루틴 중 사망
425
+
426
  💡 수사 팁:
427
  - 각 용의자를 골고루 심문하세요
428
+ - 커피, 캡슐, 주사기 관련 키워드에 주목하세요
429
+ - Elena의 심야 활동과 IRIS-01의 작업 로그를 확인하세요
430
  """
431
 
432
  def reset_game(self):
 
433
  self.state = GameState()
434
  self.conversation_history = []
435
+ self.experiment_data = {'conversations': [], 'analysis_results': {}, 'player_behavior': {}}
436
+ return True
 
 
 
 
 
 
 
437
 
438
+ def get_report_modal_html(self):
439
+ total_questions = sum(self.state.interrogation_count.values())
440
+ evidence_count = len(self.state.evidence_collected)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ if evidence_count < 2 or total_questions < 4:
443
+ return f"""
444
+ <div style="background: linear-gradient(145deg, rgba(255,0,0,0.2), rgba(200,0,0,0.3)); color: #ff6666; padding: 25px; border-radius: 15px; text-align: center; border: 2px solid #ff6666; font-family: 'Courier New', monospace; box-shadow: 0 8px 32px rgba(255, 102, 102, 0.3);">
445
+ <h3 style="color: #ff6666; margin-bottom: 18px; font-size: 20px;">⚠️ 보고서 제출 불가</h3>
446
+ <div style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 10px; margin-bottom: 15px;">
447
+ <p style="margin-bottom: 12px; font-size: 15px;">더 많은 증거와 심문이 필요합니다:</p>
448
+ <div style="margin: 8px 0; font-size: 14px;">🔍 수집된 증거: {evidence_count}/2 (최소 2개 필요)</div>
449
+ <div style="margin: 8px 0; font-size: 14px;"> 심문 횟수: {total_questions}/4 (최소 4회 필요)</div>
450
+ </div>
451
+ <p style="margin-top: 15px; font-size: 14px; color: #ffaaaa;">계속 수사를 진행해주세!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  </div>
453
+ """, gr.update(visible=True)
454
+
455
+ return self.generate_report_modal_content(), gr.update(visible=True)
456
+
457
+ def generate_report_modal_content(self):
458
+ questions_html = ""
459
+ for question in self.case_questions:
460
+ options_html = "".join([
461
+ f'<label style="display: block; margin: 8px 0; cursor: pointer; color: #ffffff; font-size: 14px; padding: 10px; border-radius: 8px; background: linear-gradient(145deg, rgba(0,0,0,0.3), rgba(20,20,40,0.3)); border: 1px solid rgba(255,255,255,0.1); transition: all 0.3s ease; text-align: left;"><input type="radio" name="question_{question["id"]}" value="{option}" style="margin-right: 12px; accent-color: #00ffff; transform: scale(1.2);"> {option}</label>'
462
+ for option in question["options"]
463
+ ])
464
+
465
+ questions_html += f"""
466
+ <div style="margin-bottom: 25px; padding: 20px; background: linear-gradient(145deg, rgba(0,0,0,0.4), rgba(20,30,50,0.4)); border-radius: 12px; border: 2px solid #00ffff; box-shadow: 0 4px 16px rgba(0, 255, 255, 0.2);">
467
+ <h4 style="color: #00ffff; margin-bottom: 15px; font-size: 16px; text-align: center; text-shadow: 0 1px 3px rgba(0,0,0,0.5);">{question["question"]}</h4>
468
+ <div style="margin: 0;">{options_html}</div>
469
+ </div>
470
+ """
471
 
472
+ script_data = {
473
+ 'questions': [q["id"] for q in self.case_questions],
474
+ 'correct_answers': {q["id"]: q["correct_answer"] for q in self.case_questions},
475
+ 'question_data': {q["id"]: {"question": q["question"], "hint": q["hint"]} for q in self.case_questions}
 
 
 
476
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
+ return f"""
479
+ <style>
480
+ @media (max-width: 768px) {{
481
+ .modal-container {{ padding: 15px !important; max-width: 95% !important; max-height: 90vh !important; }}
482
+ .modal-buttons {{ flex-direction: column !important; gap: 10px !important; }}
483
+ }}
484
+ </style>
485
 
486
+ <div id="reportModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 1000; display: none; backdrop-filter: blur(8px);">
487
+ <div class="modal-container" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(145deg, #0a0a0a, #1a1a2e); border: 3px solid #00ffff; border-radius: 15px; padding: 30px; max-width: 750px; width: 90%; max-height: 85vh; overflow-y: auto; box-shadow: 0 0 50px rgba(0, 255, 255, 0.4); font-family: 'Courier New', monospace;">
488
+ <div style="text-align: center; margin-bottom: 30px;">
489
+ <h2 style="color: #ffffff; font-size: 24px; margin-bottom: 10px;">🔍 최종 수사 보고서 🔍</h2>
490
+ <p style="color: #ffdd88; font-size: 15px;">수집한 증거를 바탕으로 사건의 진실을 밝혀내세요</p>
 
 
 
 
 
 
 
 
 
491
  </div>
492
+ <form id="reportForm">{questions_html}
493
+ <div class="modal-buttons" style="display: flex; justify-content: center; gap: 15px; margin-top: 30px;">
494
+ <button type="button" onclick="submitReport()" style="background: linear-gradient(145deg, #ff6b6b, #ee5a52); color: white; border: none; padding: 15px 30px; border-radius: 10px; font-size: 16px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);">🎯 수사 완료!</button>
495
+ <button type="button" onclick="closeModal()" style="background: linear-gradient(145deg, #666, #555); color: white; border: none; padding: 15px 30px; border-radius: 10px; font-size: 16px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.4);">취소</button>
496
+ </div>
497
+ </form>
498
+ </div>
499
  </div>
 
500
 
501
+ <div id="resultModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 1001; display: none;">
502
+ <div class="modal-container" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(145deg, #0a0a0a, #1a1a2e); border: 3px solid #FFD700; border-radius: 15px; padding: 30px; max-width: 800px; width: 90%; max-height: 85vh; overflow-y: auto; box-shadow: 0 0 50px rgba(255, 215, 0, 0.4); font-family: 'Courier New', monospace;">
503
+ <div id="resultContent"></div>
504
+ <div style="text-align: center; margin-top: 30px;">
505
+ <button type="button" onclick="closeResultModal()" style="background: linear-gradient(145deg, #00ffff, #0088cc); color: white; border: none; padding: 15px 30px; border-radius: 10px; font-size: 16px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 15px rgba(0, 255, 255, 0.4);">🔍 계속 수사하기</button>
506
+ </div>
 
 
 
 
 
 
 
 
 
 
 
507
  </div>
508
  </div>
 
509
 
510
+ <script>
511
+ const scriptData = {json.dumps(script_data, ensure_ascii=False)};
512
+ function openReportModal() {{ document.getElementById('reportModal').style.display = 'block'; }}
513
+ function closeModal() {{ document.getElementById('reportModal').style.display = 'none'; }}
514
+ function closeResultModal() {{ document.getElementById('resultModal').style.display = 'none'; }}
515
+ function submitReport() {{
516
+ const answers = {{}};
517
+ scriptData.questions.forEach(questionId => {{
518
+ const selected = document.querySelector(`input[name="question_${{questionId}}"]:checked`);
519
+ answers[questionId] = selected ? selected.value : "";
520
+ }});
521
+ let correctCount = 0;
522
+ let resultsHtml = "";
523
+ scriptData.questions.forEach(questionId => {{
524
+ const userAnswer = answers[questionId] || "미답변";
525
+ const correctAnswer = scriptData.correct_answers[questionId];
526
+ const isCorrect = userAnswer === correctAnswer;
527
+ if (isCorrect) correctCount++;
528
+ const statusIcon = isCorrect ? "" : "❌";
529
+ const answerColor = isCorrect ? "#88ff88" : "#ff8888";
530
+ resultsHtml += `<div style="margin-bottom: 15px; padding: 15px; background: linear-gradient(145deg, rgba(0,0,0,0.3), rgba(20,30,50,0.3)); border-radius: 10px; border-left: 4px solid ${{answerColor}};"><div style="color: #ffffff; margin-bottom: 8px; font-size: 15px;"><strong>${{scriptData.question_data[questionId].question}}</strong></div><div style="margin-bottom: 6px; font-size: 14px;"><span style="color: #ffdd88;">당신의 답:</span> <span style="color: ${{answerColor}}; font-weight: bold;">${{userAnswer}} ${{statusIcon}}</span></div><div style="margin-bottom: 6px; font-size: 14px;"><span style="color: #ffdd88;">정답:</span> <span style="color: #88ff88; font-weight: bold;">${{correctAnswer}}</span></div>${{!isCorrect ? `<div style="color: #aabbcc; font-size: 13px; font-style: italic; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 5px; margin-top: 8px;">💡 ${{scriptData.question_data[questionId].hint}}</div>` : ""}}</div>`;
531
+ }});
532
+ const totalQuestions = scriptData.questions.length;
533
+ const scorePercentage = (correctCount / totalQuestions) * 100;
534
+ let grade, gradeColor, finalMessage;
535
+ if (scorePercentage >= 90) {{
536
+ grade = "S급 탐정"; gradeColor = "#FFD700";
537
+ finalMessage = "완벽한 추리력! 당신은 진정한 사이버펑크 탐정입니다! 🕵️‍♂️⭐";
538
+ }} else if (scorePercentage >= 80) {{
539
+ grade = "A급 탐정"; gradeColor = "#00FF00";
540
+ finalMessage = "훌륭한 수사 실력! 대부분의 진실을 밝혀냈습니���! 🔍✨";
541
+ }} else if (scorePercentage >= 70) {{
542
+ grade = "B급 탐정"; gradeColor = "#00AAFF";
543
+ finalMessage = "좋은 추리! 몇 가지 단서를 놓쳤지만 사건을 해결했습니다! 🎯";
544
+ }} else {{
545
+ grade = "수습 탐정"; gradeColor = "#FF6666";
546
+ finalMessage = "더 많은 증거 수집이 필요했습니다. 다시 도전해보세요! 💪";
547
+ }}
548
+ const finalHtml = `<div style="text-align: center; margin-bottom: 25px;"><h2 style="color: ${{gradeColor}}; font-size: 26px; margin-bottom: 10px;">🏆 사건 수사 완료! 🏆</h2><div style="color: ${{gradeColor}}; font-size: 20px; font-weight: bold; margin-bottom: 8px;">${{grade}}</div><div style="color: #ffffff; font-size: 16px; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 8px; display: inline-block;">정답률: ${{correctCount}}/${{totalQuestions}} (${{scorePercentage.toFixed(1)}}%)</div></div><div style="margin-bottom: 20px;"><h3 style="color: #00ffff; margin-bottom: 15px; font-size: 18px; text-align: center;">📋 수사 결과</h3>${{resultsHtml}}</div><div style="text-align: center; margin-top: 20px; padding: 18px; background: linear-gradient(145deg, rgba(0,255,255,0.1), rgba(0,200,255,0.1)); border-radius: 12px; border: 1px solid rgba(0,255,255,0.3);"><div style="color: #ffffff; font-size: 15px;">${{finalMessage}}</div></div>`;
549
+ document.getElementById('resultContent').innerHTML = finalHtml;
550
+ document.getElementById('reportModal').style.display = 'none';
551
+ document.getElementById('resultModal').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }}
553
+ setTimeout(() => openReportModal(), 100);
554
+ </script>
555
+ """
556
 
557
+ # 게임 인스턴스 생성
558
+ game = CyberpunkGame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
+ # Gradio UI 함수들
561
+ def clear_game():
562
+ game.reset_game()
563
+ return (game.create_chat_html(), "", game.get_interrogation_info_html('Elena'), game.get_report_status_html())
 
 
 
564
 
565
+ def interrogate_and_update_info(message, suspect_name):
566
+ chat_html, empty_input = game.interrogate_suspect(message, suspect_name)
567
+ return (chat_html, empty_input, game.get_interrogation_info_html(suspect_name), game.get_report_status_html())
 
568
 
569
+ def update_character_info_and_display(suspect_name):
570
+ game.state.current_suspect = suspect_name
571
+ return (game.get_character_info_html(suspect_name), game.get_interrogation_info_html(suspect_name), game.create_chat_html(suspect_name))
 
 
 
 
 
572
 
573
  # Gradio 인터페이스 생성
574
  with gr.Blocks(title="사이버펑크 추리 게임", theme=gr.themes.Monochrome()) as demo:
575
+ gr.HTML("""
576
+ <style>
577
+ @media (max-width: 768px) {
578
+ .gradio-container { padding: 10px !important; }
579
+ .gr-button { font-size: 14px !important; padding: 12px 16px !important; margin: 4px !important; }
580
+ }
581
+ .gr-button:hover { transform: translateY(-1px) !important; }
582
+ </style>
583
+ """)
584
+
585
  gr.HTML(f"""
586
+ <div style="text-align: center; background: linear-gradient(90deg, #000, #0a0a0a, #000); color: #ffffff; padding: 20px; border-radius: 12px; margin-bottom: 20px; {Styles.NEON_BORDER}; box-shadow: {Styles.NEON_SHADOW};">
587
+ <h1 style="font-family: 'Courier New', monospace; font-size: clamp(22px, 5vw, 30px); margin-bottom: 10px; color: #ffffff;">🔍 CYBERPUNK MURDER INVESTIGATION 🤖</h1>
588
+ <p style="font-size: clamp(13px, 3vw, 15px); color: #ffd93d;">근미래 사이버펑크 도시에서 발생한 Alexander 독살 사건을 해결하세요</p>
 
 
 
 
 
 
 
589
  </div>
590
  """)
591
 
592
  with gr.Row():
593
  with gr.Column(scale=3):
594
+ chat_display = gr.HTML(value=game.create_chat_html(), label="심문실")
595
+ interrogation_info = gr.HTML(value=game.get_interrogation_info_html('Elena'), label="")
 
 
 
 
 
 
 
 
 
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  with gr.Row():
598
+ message_input = gr.Textbox(placeholder="자에게 질문입력하세요...", label="", scale=4, container=False, lines=1)
599
+ send_btn = gr.Button("🔍 질문", variant="primary", scale=1, size="lg")
600
+
601
+ gr.HTML("""
602
+ <div style="margin: 15px 0 10px 0; padding: 15px;
603
+ background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
604
+ border-radius: 10px; text-align: center; border: 2px solid rgba(255, 255, 255, 0.1);
605
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
606
+ display: flex; align-items: center; justify-content: center; min-height: 50px;">
607
+ <div style="color: #ffdd88; font-weight: bold; font-size: 16px; text-shadow: 0 1px 3px rgba(0,0,0,0.5);">💭 빠른 질문 버튼</div>
608
+ </div>
609
+ """)
610
+
611
+ with gr.Column():
612
+ with gr.Row():
613
+ quick1 = gr.Button("🕐 사건 당일 어디에 있었나?", size="sm", variant="secondary")
614
+ quick2 = gr.Button("💔 Alexander와 어떤 관계였나?", size="sm", variant="secondary")
615
+ with gr.Row():
616
+ quick3 = gr.Button("👁️ 의심스러운 행동을 본 적 있나?", size="sm", variant="secondary")
617
+ quick4 = gr.Button("🤐 숨기고 있는 게 있다면 말해달라", size="sm", variant="secondary")
618
 
619
  with gr.Column(scale=1):
620
+ gr.HTML("""
621
+ <div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
622
+ padding: 20px; border-radius: 12px; margin-bottom: 15px;
623
+ border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
624
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
625
+ display: flex; align-items: center; justify-content: center; min-height: 60px;">
626
+ <h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
627
+ text-shadow: 0 1px 3px rgba(0,0,0,0.5);">🤖 용의자 선택</h3>
628
+ </div>
629
+ """)
630
 
631
  suspect_choice = gr.Radio(
632
+ choices=[("👩 Elena (아내)", "Elena"), ("🤖 IRIS-01 (가정부 로봇)", "IRIS-01"), ("👨‍🔬 Dr. Chen (개발자)", "Dr. Chen"), ("🖥️ ZEN (보안 AI)", "ZEN")],
633
+ value="Elena", label="심문할 용의자", info="각 용의자를 선택하면 해당 캐릭터와 대화할 수 있습니다"
 
 
 
 
 
 
 
634
  )
635
 
 
636
  with gr.Group():
637
+ character_info = gr.HTML(value=game.get_character_info_html('Elena'), label="")
638
+
639
+ gr.HTML("""
640
+ <div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
641
+ padding: 20px; border-radius: 12px; margin-bottom: 15px;
642
+ border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
643
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
644
+ display: flex; align-items: center; justify-content: center; min-height: 60px;">
645
+ <h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
646
+ text-shadow: 0 1px 3px rgba(0,0,0,0.5);">🎯 최종 보고서</h3>
647
+ </div>
648
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
+ submit_report_btn = gr.Button("📋 최종 보고서 제출", variant="primary", size="lg", visible=True)
651
+ report_status = gr.HTML(value=game.get_report_status_html(), label="")
652
+ modal_container = gr.HTML(value="", label="", visible=False)
653
+
654
+ gr.HTML("""
655
+ <div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
656
+ padding: 20px; border-radius: 12px; margin-bottom: 15px;
657
+ border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
658
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
659
+ display: flex; align-items: center; justify-content: center; min-height: 60px;">
660
+ <h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
661
+ text-shadow: 0 1px 3px rgba(0,0,0,0.5);">📊 수사 도구</h3>
662
+ </div>
663
+ """)
664
 
665
+ with gr.Column():
666
+ case_summary_btn = gr.Button("📋 수사 현황 보기", variant="secondary", size="lg")
667
+ case_summary_output = gr.Textbox(label="📊 수사 리포트", lines=14, interactive=False, visible=False)
668
+ clear_btn = gr.Button("🔄 수사 초기화", variant="stop", size="lg")
669
 
670
  # 이벤트 핸들러
671
+ send_btn.click(interrogate_and_update_info, inputs=[message_input, suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
672
+ message_input.submit(interrogate_and_update_info, inputs=[message_input, suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
673
+
674
+ def send_quick_question(question, suspect):
 
 
 
 
 
 
 
 
 
 
 
675
  return interrogate_and_update_info(question, suspect)
676
 
677
+ quick1.click(lambda s: send_quick_question("사건 당일 어디에 있었나요?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
678
+ quick2.click(lambda s: send_quick_question("Alexander와 관계였?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
679
+ quick3.click(lambda s: send_quick_question("의심스러운 행동을 본 적 있나요?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
680
+ quick4.click(lambda s: send_quick_question("숨기고 있는 게 있다면 말해달라", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
681
+
682
+ suspect_choice.change(update_character_info_and_display, inputs=[suspect_choice], outputs=[character_info, interrogation_info, chat_display])
683
+ submit_report_btn.click(game.get_report_modal_html, outputs=[modal_container, modal_container])
684
+ case_summary_btn.click(lambda: (game.get_case_summary(), gr.update(visible=True)), outputs=[case_summary_output, case_summary_output])
685
+ clear_btn.click(clear_game, outputs=[chat_display, message_input, interrogation_info, report_status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
 
687
  # 인터페이스 실행
688
  if __name__ == "__main__":