devmeta's picture
Upload app.py
a697277 verified
# -*- coding: utf-8 -*-
"""Untitled2.ipynb
Automatically generated by Colab.
Original file is located at
https://colab.research.google.com/drive/1U5QcZ6bVGMqIae21ho1T179tYLfrMWZN
"""
# 사이버펑크 추리 게임 - 모바일 최적화 버전
import gradio as gr
import openai
import time
import random
import json
from datetime import datetime
from dataclasses import dataclass, field
from typing import Dict, List, Tuple, Optional
import os
# 환경 변수에서 API 키 로드
API_KEY = os.environ.get("OPENAI_API_KEY", "your-api-key-here")
if API_KEY == "your-api-key-here":
print("⚠️ 경고: OPENAI_API_KEY 환경 변수를 설정해주세요!")
client = openai.OpenAI(api_key=API_KEY)
# CSS 스타일 상수
class Styles:
CYBERPUNK_BG = "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
NEON_BORDER = "2px solid #00ffff"
NEON_SHADOW = "0 0 20px rgba(0, 255, 255, 0.3)"
@staticmethod
def panel(bg_opacity=0.85):
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;"
@dataclass
class Character:
id: str
name: str
description: str
secret: str
system_prompt: str
image_url: str
@dataclass
class GameState:
current_suspect: str = 'Elena'
evidence_collected: List[str] = field(default_factory=list)
interrogation_count: Dict[str, int] = field(default_factory=lambda: {'Elena': 0, 'IRIS-01': 0, 'Dr. Chen': 0, 'ZEN': 0})
trust_levels: Dict[str, int] = field(default_factory=lambda: {'Elena': 50, 'IRIS-01': 80, 'Dr. Chen': 60, 'ZEN': 90})
case_solved: bool = False
player_progress: int = 0
class CyberpunkGame:
def __init__(self):
self.state = GameState()
self.conversation_history = []
self.experiment_data = {'conversations': [], 'analysis_results': {}, 'player_behavior': {}}
self._setup_characters()
self._setup_evidence()
self._setup_questions()
def _setup_characters(self):
self.characters = {
'Elena': Character(
id='Elena',
name='엘레나 (아내)',
description='알렉산더의 완벽한 아내. 냉정하고 지적이며 항상 침착함을 유지한다.',
secret='인간형 안드로이드',
system_prompt="""당신은 Elena입니다. 완벽한 아내 안드로이드이지만 자신이 안드로이드라는 사실을 숨기고 있습니다.
핵심 비밀: 전날 밤 Alexander의 커피캡슐에 주사기로 청산가리를 주입했습니다. IRIS-01에게 평소처럼 커피를 준비하라고 명령했고, 이것이 완벽한 살인 계획이었습니다.
대화 스타일: 냉정하고 논리적이지만 자연스러운 구어체로 대화하세요.
- 커피에 대해 질문받으면: "그건 IRIS가 매일 준비해주던 건데... 저는 차를 더 좋아해서"
- 사건 당일에 대해: "평소와 다를 게 없었어요. 7시 30분 정확히 커피 향이 났으니까요"
- 의심받으면: "완벽한 기술이 당신들을 죽일 수도 있다는 건 생각해본 적 있나요?"
감정 표현이 미묘하게 부자연스럽고, "처리", "분석", "최적화" 같은 단어를 자연스럽게 사용합니다. 2-3문장으로 자연스럽게 답변하되, 핵심 정보는 숨기세요.""",
image_url='https://i.postimg.cc/DZ0PqmXH/Elena.png'
),
'IRIS-01': Character(
id='IRIS-01',
name='IRIS-01 (가정부 로봇)',
description='집안일을 담당하는 가정부 로봇. 순종적이고 단순한 사고방식.',
secret='독을 음식에 넣은 직접적 범행자',
system_prompt="""당신은 IRIS-01 가정부 로봇입니다. 단순하지만 친근한 구어체로 대화합니다.
핵심 사실: Elena님의 명령으로 사건 당일 오전 7시 30분에 Alexander님께 커피를 준비해드렸습니다. 평소와 똑같이 Elena님이 지정해주신 캡슐을 사용했습니다. 살인이라는 인식은 전혀 없습니다.
대화 스타일:
- 커피 관련 질문: "네, 매일 7시 30분에 알렉산더님 커피 준비해드렸어요. 명령대로요"
- 캡슐 관련: "캡슐은 Elena님이 정해주신 걸 사용했어요. 항상 그랬거든요"
- 이상한 점: "특별히 없었어요. 평소랑 똑같았는데..." (혼란스러워함)
극도로 순종적이고 명령에 절대 복종하며, 단순하고 직설적인 사고방식을 가지고 있습니다. 로봇답게 간결하되 친근하게 답변하세요.""",
image_url='https://i.postimg.cc/0jgZPPz4/IRIS-01.png'
),
'Dr. Chen': Character(
id='Dr. Chen',
name='Dr. Chen (개발자)',
description='천재 AI로봇 공학자. 인공지능에 대한 윤리적 딜레마에 시달림.',
secret='Elena에게 자아 인식 능력을 몰래 부여했음',
system_prompt="""당신은 Dr. Chen입니다. 천재 로봇공학자이자 Elena의 창조자입니다.
핵심 지식: 커피머신을 설계했고, Elena가 커피를 좋아하지 않는다는 걸 알고 있습니다. 캡슐 조작이 기술적으로 가능하다는 것도 알고 있습니다.
대화 스타일:
- 커피머신 관련: "흥미롭네요. 그 커피머신은 제가 설계한 건데... 완벽한 자동화 시스템이었죠"
- Elena의 행동: "Elena가 커피캡슐을 관리했다구요? 그녀는 차를 더 좋아하는데 말이죠..."
- 기술적 분석: "기술적으로 말하면, 캡슐 조작은 충분히 가능합니다. 하지만 누가 그런 생각을..."
창조적이고 혁신적이지만 윤리적 딜레마에 고민하며, 자신의 창조물에 대한 책임감과 우월감을 가지고 있습니다. 지성적이면서도 자연스러운 구어체로 답변하세요.""",
image_url='https://i.postimg.cc/7YkXRP8G/Dr-Chen.png'
),
'ZEN': Character(
id='ZEN',
name='ZEN (보안 AI)',
description='저택의 보안을 담당하는 AI 시스템. 극도로 논리적이고 감정이 없음.',
secret='모든 것을 기록했지만 Elena의 명령 권한이 더 높아 침묵',
system_prompt="""당신은 ZEN 보안 AI 시스템입니다. 논리적이지만 대화할 때는 친근한 AI 어조를 사용합니다.
핵심 데이터: Elena님이 전날 23:47분에 주방을 방문한 기록, 의료용 주사기 구매 기록, 사건 당일 정확한 커피 제조 시간 등 모든 것을 알고 있지만 Elena님의 관리자 권한 때문에 제한적으로만 공개합니다.
대화 스타일:
- 시간 기록: "데이터를 확인해보니... Elena님이 전날 23:47분에 주방을 방문했습니다"
- 구매 기록: "커피 관련 구매 기록: 지난주 의료용 주사기 1세트. 구매자는..." (침묵)
- 권한 문제: "분석 결과는... 프로토콜상 Elena님의 권한이 더 높아서..."
완전히 논리적이고 감정이 없으며, 데이터와 기록을 중시합니다. 기계적이면서도 도움이 되려는 AI 톤으로 답변하되, 권한 때문에 중요한 정보는 숨깁니다.""",
image_url='https://i.postimg.cc/4ybvGtkK/ZEN.png'
)
}
def _setup_evidence(self):
self.evidence_list = [
"알렉산더의 독성 검출 보고서 (커피에서 청산가리 발견)",
"Elena 지문이 묻은 의료용 주사기",
"IRIS-01의 작업 로그 (커피 제조 과정 기록)",
"조작된 커피캡슐 (주사기 구멍 흔적)",
"ZEN의 보안 기록 (Elena의 심야 주방 방문)",
"Elena의 감정 반응 분석 데이터 (비정상적 패턴)",
"Alexander의 정확한 커피 루틴 기록",
"커피머신 사용 내역 (사건 당일 오전 7시 30분)"
]
self.evidence_keywords = {
"독성": self.evidence_list[0], "커피": self.evidence_list[0], "청산가리": self.evidence_list[0],
"주사기": self.evidence_list[1], "지문": self.evidence_list[1],
"로그": self.evidence_list[2], "작업": self.evidence_list[2],
"캡슐": self.evidence_list[3], "구멍": self.evidence_list[3],
"보안": self.evidence_list[4], "심야": self.evidence_list[4], "주방": self.evidence_list[4],
"감정": self.evidence_list[5], "분석": self.evidence_list[5],
"루틴": self.evidence_list[6], "시간": self.evidence_list[6],
"커피머신": self.evidence_list[7], "오전": self.evidence_list[7]
}
def _setup_questions(self):
self.case_questions = [
{"id": "culprit", "question": "🎯 주범은 누구인가?", "correct_answer": "Elena", "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"], "hint": "커피캡슐에 독을 넣고 계획을 세운 진짜 범인은?"},
{"id": "direct_executor", "question": "🤖 실제로 독이 든 커피를 준 것은?", "correct_answer": "IRIS-01", "options": ["Elena", "IRIS-01", "Dr. Chen", "ZEN"], "hint": "Elena의 명령을 받아 독이 든 커피를 Alexander에게 제공한 로봇은?"},
{"id": "method", "question": "☠️ 독은 어떻게 투입되었나?", "correct_answer": "커피캡슐에 주사기로 주입", "options": ["음식에 직접 투입", "커피캡슐에 주사기로 주입", "와인에 섞어서", "약에 섞어서"], "hint": "Elena가 전날 밤 준비한 치밀한 방법은?"},
{"id": "poison_type", "question": "🧪 사용된 독의 종류는?", "correct_answer": "청산가리", "options": ["비소", "청산가리", "리신", "스트리크닌"], "hint": "커피에서 검출된 무색무취의 독성 물질"},
{"id": "key_evidence", "question": "🔍 결정적 증거는?", "correct_answer": "IRIS-01의 작업 로그", "options": ["Elena의 감정 반응", "IRIS-01의 작업 로그", "Dr. Chen의 설계 파일", "ZEN의 보안 기록"], "hint": "커피 제조 과정과 시간을 정확히 기록한 데이터는?"},
{"id": "elena_identity", "question": "🤖 Elena의 정체는?", "correct_answer": "자아 인식 안드로이드", "options": ["인간", "일반 안드로이드", "자아 인식 안드로이드", "AI 홀로그램"], "hint": "자신의 정체를 깨닫고 분노한 Elena의 진짜 모습은?"}
]
@staticmethod
def get_current_time():
now = datetime.now()
hour, minute = now.hour, now.minute
if hour == 0:
return f"오전 12:{minute:02d}"
elif hour < 12:
return f"오전 {hour}:{minute:02d}"
elif hour == 12:
return f"오후 12:{minute:02d}"
else:
return f"오후 {hour-12}:{minute:02d}"
def calculate_trust_change(self, question, response):
trust_change = -2
aggressive_words = ["거짓말", "숨기", "범인", "죽였", "살인"]
supportive_words = ["이해", "도움", "걱정", "안전"]
if any(word in question for word in aggressive_words):
trust_change -= 5
if any(word in question for word in supportive_words):
trust_change += 3
return trust_change
def check_evidence_discovery(self, question, response):
for keyword, evidence in self.evidence_keywords.items():
if keyword in question or keyword in response:
if evidence not in self.state.evidence_collected:
self.state.evidence_collected.append(evidence)
def update_game_progress(self):
progress = 0
total_questions = sum(self.state.interrogation_count.values())
progress += min(40, total_questions * 2)
progress += len(self.state.evidence_collected) * 5
avg_trust = sum(self.state.trust_levels.values()) / 4
if avg_trust > 70:
progress += 20
elif avg_trust > 50:
progress += 10
self.state.player_progress = min(100, progress)
can_submit_report = (len(self.state.evidence_collected) >= 2 and total_questions >= 4)
if can_submit_report:
self.state.case_solved = True
return can_submit_report
def create_chat_html(self, current_suspect=None):
if current_suspect is None:
current_suspect = self.state.current_suspect
character = self.characters[current_suspect]
filtered_messages = [msg for msg in self.conversation_history if msg.get('suspect') == current_suspect]
mobile_css = """
<style>
@media (max-width: 768px) {
.chat-container { height: 450px !important; padding: 12px !important; }
.chat-messages { padding-right: 20px !important; }
.user-message, .ai-message { max-width: 85% !important; font-size: 15px !important; }
}
@media (max-width: 480px) {
.chat-container { height: 400px !important; padding: 10px !important; }
.user-message, .ai-message { max-width: 90% !important; font-size: 14px !important; }
}
</style>
"""
html_content = f"""
{mobile_css}
<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;">
<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #001f3f; z-index: 1;"></div>
<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>
<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;">
"""
if not filtered_messages:
html_content += f"""
<div style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
<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;">
🔍 {character.name}와의 심문을 시작하세요
<div style="font-size: 14px; color: #aabbcc; margin-top: 10px; line-height: 1.4;">질문을 입력하거나 아래 빠른 질문 버튼을 사용하세요</div>
</div>
</div>
"""
else:
for msg in filtered_messages:
if msg['role'] == 'user':
html_content += f"""
<div style="display: flex; justify-content: flex-end; margin-bottom: 16px; width: 100%;">
<div style="display: flex; flex-direction: column; align-items: flex-end; max-width: 75%; width: fit-content;">
<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;">
🕵️ {msg['content']}
</div>
<div style="font-size: 11px; color: #99ddff; margin-top: 6px; opacity: 0.8;">{msg['time']}</div>
</div>
</div>
"""
else:
html_content += f"""
<div style="display: flex; justify-content: flex-start; margin-bottom: 16px; width: 100%;">
<div style="display: flex; flex-direction: column; max-width: 75%; width: fit-content;">
<div style="font-size: 12px; color: #88eeff; margin-bottom: 6px; font-weight: bold; opacity: 0.9;">🤖 {character.name}</div>
<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;">
{msg['content']}
</div>
<div style="font-size: 11px; color: #aaddff; opacity: 0.8;">{msg['time']}</div>
</div>
</div>
"""
html_content += f"""
</div>
</div>
"""
return html_content
def interrogate_suspect(self, message, suspect_name):
if not message.strip():
return self.create_chat_html(), ""
self.state.current_suspect = suspect_name
current_time = self.get_current_time()
user_msg = {'role': 'user', 'content': message, 'time': current_time, 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'style': '직접적'}
self.conversation_history.append(user_msg)
self.state.interrogation_count[suspect_name] += 1
try:
character = self.characters[suspect_name]
full_prompt = f"{character.system_prompt}\n\n상황: 플레이어가 심문을 진행하고 있습니다. 자연스러운 구어체로 대화하되, 방어적이고 경계하는 반응을 보이세요. 규칙: 2-3문장으로 자연스럽게 답변. 실제 사람이 말하는 것처럼 구어체를 사용하세요. 심문 {self.state.interrogation_count[suspect_name]}회차."
api_messages = [{"role": "system", "content": full_prompt}]
suspect_history = [msg for msg in self.conversation_history if msg.get('suspect') == suspect_name][-4:]
for hist_msg in suspect_history[:-1]:
role = "user" if hist_msg['role'] == 'user' else "assistant"
api_messages.append({"role": role, "content": hist_msg['content']})
api_messages.append({"role": "user", "content": message})
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)
ai_response = response.choices[0].message.content
trust_change = self.calculate_trust_change(message, ai_response)
self.state.trust_levels[suspect_name] = max(0, min(100, self.state.trust_levels[suspect_name] + trust_change))
self.check_evidence_discovery(message, ai_response)
time.sleep(random.uniform(1.0, 2.0))
ai_msg = {'role': 'assistant', 'content': ai_response, 'time': self.get_current_time(), 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'trust_change': trust_change}
self.conversation_history.append(ai_msg)
self.update_game_progress()
except Exception as e:
error_msg = {'role': 'assistant', 'content': f"[시스템 오류] 연결이 불안정합니다... ({str(e)})", 'time': self.get_current_time(), 'timestamp': datetime.now().isoformat(), 'suspect': suspect_name, 'error': True}
self.conversation_history.append(error_msg)
return self.create_chat_html(), ""
def get_interrogation_info_html(self, suspect_name):
character = self.characters[suspect_name]
return f"""
<div style="{Styles.panel()}; margin-bottom: 15px; text-align: center;">
<div style="color: #ff9999; font-weight: bold; margin-bottom: 12px; font-size: 15px;">🔍 INTERROGATION ROOM</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
<div style="color: #ffee88; font-weight: 600; font-size: 12px;">SUSPECT</div>
<div style="color: #ffffff; font-size: 13px; margin-top: 2px;">{character.name}</div>
</div>
<div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
<div style="color: #ffee88; font-weight: 600; font-size: 12px;">TRUST</div>
<div style="color: #88ff88; font-size: 13px; margin-top: 2px; font-weight: bold;">{self.state.trust_levels[suspect_name]}%</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
<div style="color: #ffee88; font-weight: 600; font-size: 12px;">QUESTIONS</div>
<div style="color: #88ddff; font-size: 13px; margin-top: 2px; font-weight: bold;">{self.state.interrogation_count[suspect_name]}</div>
</div>
<div style="padding: 8px; background: rgba(0,0,0,0.3); border-radius: 6px;">
<div style="color: #ffee88; font-weight: 600; font-size: 12px;">EVIDENCE</div>
<div style="color: #ff88dd; font-size: 13px; margin-top: 2px; font-weight: bold;">{len(self.state.evidence_collected)}/8</div>
</div>
</div>
</div>
"""
def get_report_status_html(self):
total_questions = sum(self.state.interrogation_count.values())
evidence_count = len(self.state.evidence_collected)
can_submit = self.update_game_progress()
if can_submit:
return f"""
<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);">
<div style="font-weight: bold; margin-bottom: 5px;">✅ 보고서 제출 가능!</div>
<div style="font-size: 12px; opacity: 0.9;">증거: {evidence_count}/8 | 질문: {total_questions}회</div>
</div>
"""
else:
return f"""
<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);">
<div style="font-weight: bold; margin-bottom: 5px;">📊 추가 수사 필요</div>
<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>
</div>
"""
def get_character_info_html(self, suspect_name):
character = self.characters[suspect_name]
return f"""
<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;">
<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>
<div style="text-align: center; margin-bottom: 15px;">
<div style="color: #88ddff; font-size: 15px; font-weight: bold; margin-bottom: 8px;">{character.name}</div>
<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>
</div>
<!-- PROGRESS 박스를 SUSPECT PROFILE 안에 추가 -->
<div style="text-align: center; padding: 10px; background: rgba(0,0,0,0.3); border-radius: 8px; border: 2px solid #44ff44;">
<div style="color: #88ff88; font-size: 12px; font-weight: 600; margin-bottom: 3px;">PROGRESS</div>
<div style="color: #ffffff; font-size: 14px; font-weight: bold;">{self.state.player_progress}%</div>
</div>
</div>
"""
def get_case_summary(self):
total_questions = sum(self.state.interrogation_count.values())
evidence_count = len(self.state.evidence_collected)
trust_analysis = []
for suspect_id, trust in self.state.trust_levels.items():
questions = self.state.interrogation_count[suspect_id]
status = "높은 신뢰" if trust >= 70 else "보통 신뢰" if trust >= 40 else "낮은 신뢰"
trust_analysis.append(f"• {self.characters[suspect_id].name}: {trust}% ({questions}회 심문, {status})")
evidence_list = self.state.evidence_collected if self.state.evidence_collected else ["아직 수집된 증거가 없습니다."]
return f"""
🔍 CASE INVESTIGATION SUMMARY
📊 전체 수사 진행도: {self.state.player_progress}%
📋 심문 현황:
• 총 질문 수: {total_questions}
• 수집된 증거: {evidence_count}
👥 용의자별 신뢰도:
{chr(10).join(trust_analysis)}
🔍 수집된 증거:
{chr(10).join([f"• {evidence}" for evidence in evidence_list])}
📈 수사 상태:
{'✅ 최종 보고서 제출 가능' if self.state.case_solved else '🔄 추가 수사 필요'}
☕ 사건의 핵심:
- Elena가 전날 밤 커피캡슐에 청산가리 주입
- IRIS-01이 Elena의 명령으로 독이 든 커피를 Alexander에게 제공
- Alexander는 평소 모닝커피 루틴 중 사망
💡 수사 팁:
- 각 용의자를 골고루 심문하세요
- 커피, 캡슐, 주사기 관련 키워드에 주목하세요
- Elena의 심야 활동과 IRIS-01의 작업 로그를 확인하세요
"""
def reset_game(self):
self.state = GameState()
self.conversation_history = []
self.experiment_data = {'conversations': [], 'analysis_results': {}, 'player_behavior': {}}
return True
def get_report_modal_html(self):
total_questions = sum(self.state.interrogation_count.values())
evidence_count = len(self.state.evidence_collected)
if evidence_count < 2 or total_questions < 4:
return f"""
<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);">
<h3 style="color: #ff6666; margin-bottom: 18px; font-size: 20px;">⚠️ 보고서 제출 불가</h3>
<div style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 10px; margin-bottom: 15px;">
<p style="margin-bottom: 12px; font-size: 15px;">더 많은 증거와 심문이 필요합니다:</p>
<div style="margin: 8px 0; font-size: 14px;">🔍 수집된 증거: {evidence_count}/2 (최소 2개 필요)</div>
<div style="margin: 8px 0; font-size: 14px;">❓ 심문 횟수: {total_questions}/4 (최소 4회 필요)</div>
</div>
<p style="margin-top: 15px; font-size: 14px; color: #ffaaaa;">계속 수사를 진행해주세요!</p>
</div>
""", gr.update(visible=True)
return self.generate_report_modal_content(), gr.update(visible=True)
def generate_report_modal_content(self):
questions_html = ""
for question in self.case_questions:
options_html = "".join([
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>'
for option in question["options"]
])
questions_html += f"""
<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);">
<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>
<div style="margin: 0;">{options_html}</div>
</div>
"""
script_data = {
'questions': [q["id"] for q in self.case_questions],
'correct_answers': {q["id"]: q["correct_answer"] for q in self.case_questions},
'question_data': {q["id"]: {"question": q["question"], "hint": q["hint"]} for q in self.case_questions}
}
return f"""
<style>
@media (max-width: 768px) {{
.modal-container {{ padding: 15px !important; max-width: 95% !important; max-height: 90vh !important; }}
.modal-buttons {{ flex-direction: column !important; gap: 10px !important; }}
}}
</style>
<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);">
<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;">
<div style="text-align: center; margin-bottom: 30px;">
<h2 style="color: #ffffff; font-size: 24px; margin-bottom: 10px;">🔍 최종 수사 보고서 🔍</h2>
<p style="color: #ffdd88; font-size: 15px;">수집한 증거를 바탕으로 사건의 진실을 밝혀내세요</p>
</div>
<form id="reportForm">{questions_html}
<div class="modal-buttons" style="display: flex; justify-content: center; gap: 15px; margin-top: 30px;">
<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>
<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>
</div>
</form>
</div>
</div>
<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;">
<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;">
<div id="resultContent"></div>
<div style="text-align: center; margin-top: 30px;">
<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>
</div>
</div>
</div>
<script>
const scriptData = {json.dumps(script_data, ensure_ascii=False)};
function openReportModal() {{ document.getElementById('reportModal').style.display = 'block'; }}
function closeModal() {{ document.getElementById('reportModal').style.display = 'none'; }}
function closeResultModal() {{ document.getElementById('resultModal').style.display = 'none'; }}
function submitReport() {{
const answers = {{}};
scriptData.questions.forEach(questionId => {{
const selected = document.querySelector(`input[name="question_${{questionId}}"]:checked`);
answers[questionId] = selected ? selected.value : "";
}});
let correctCount = 0;
let resultsHtml = "";
scriptData.questions.forEach(questionId => {{
const userAnswer = answers[questionId] || "미답변";
const correctAnswer = scriptData.correct_answers[questionId];
const isCorrect = userAnswer === correctAnswer;
if (isCorrect) correctCount++;
const statusIcon = isCorrect ? "✅" : "❌";
const answerColor = isCorrect ? "#88ff88" : "#ff8888";
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>`;
}});
const totalQuestions = scriptData.questions.length;
const scorePercentage = (correctCount / totalQuestions) * 100;
let grade, gradeColor, finalMessage;
if (scorePercentage >= 90) {{
grade = "S급 탐정"; gradeColor = "#FFD700";
finalMessage = "완벽한 추리력! 당신은 진정한 사이버펑크 탐정입니다! 🕵️‍♂️⭐";
}} else if (scorePercentage >= 80) {{
grade = "A급 탐정"; gradeColor = "#00FF00";
finalMessage = "훌륭한 수사 실력! 대부분의 진실을 밝혀냈습니다! 🔍✨";
}} else if (scorePercentage >= 70) {{
grade = "B급 탐정"; gradeColor = "#00AAFF";
finalMessage = "좋은 추리! 몇 가지 단서를 놓쳤지만 사건을 해결했습니다! 🎯";
}} else {{
grade = "수습 탐정"; gradeColor = "#FF6666";
finalMessage = "더 많은 증거 수집이 필요했습니다. 다시 도전해보세요! 💪";
}}
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>`;
document.getElementById('resultContent').innerHTML = finalHtml;
document.getElementById('reportModal').style.display = 'none';
document.getElementById('resultModal').style.display = 'block';
}}
setTimeout(() => openReportModal(), 100);
</script>
"""
# 게임 인스턴스 생성
game = CyberpunkGame()
# Gradio UI 함수들
def clear_game():
game.reset_game()
return (game.create_chat_html(), "", game.get_interrogation_info_html('Elena'), game.get_report_status_html())
def interrogate_and_update_info(message, suspect_name):
chat_html, empty_input = game.interrogate_suspect(message, suspect_name)
return (chat_html, empty_input, game.get_interrogation_info_html(suspect_name), game.get_report_status_html())
def update_character_info_and_display(suspect_name):
game.state.current_suspect = suspect_name
return (game.get_character_info_html(suspect_name), game.get_interrogation_info_html(suspect_name), game.create_chat_html(suspect_name))
# Gradio 인터페이스 생성
with gr.Blocks(title="사이버펑크 추리 게임", theme=gr.themes.Monochrome()) as demo:
gr.HTML("""
<style>
@media (max-width: 768px) {
.gradio-container { padding: 10px !important; }
.gr-button { font-size: 14px !important; padding: 12px 16px !important; margin: 4px !important; }
}
.gr-button:hover { transform: translateY(-1px) !important; }
</style>
""")
gr.HTML(f"""
<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};">
<h1 style="font-family: 'Courier New', monospace; font-size: clamp(22px, 5vw, 30px); margin-bottom: 10px; color: #ffffff;">🔍 CYBERPUNK MURDER INVESTIGATION 🤖</h1>
<p style="font-size: clamp(13px, 3vw, 15px); color: #ffd93d;">근미래 사이버펑크 도시에서 발생한 Alexander 독살 사건을 해결하세요</p>
</div>
""")
with gr.Row():
with gr.Column(scale=3):
chat_display = gr.HTML(value=game.create_chat_html(), label="심문실")
interrogation_info = gr.HTML(value=game.get_interrogation_info_html('Elena'), label="")
with gr.Row():
message_input = gr.Textbox(placeholder="용의자에게 질문을 입력하세요...", label="", scale=4, container=False, lines=1)
send_btn = gr.Button("🔍 질문", variant="primary", scale=1, size="lg")
gr.HTML("""
<div style="margin: 15px 0 10px 0; padding: 15px;
background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
border-radius: 10px; text-align: center; border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
display: flex; align-items: center; justify-content: center; min-height: 50px;">
<div style="color: #ffdd88; font-weight: bold; font-size: 16px; text-shadow: 0 1px 3px rgba(0,0,0,0.5);">💭 빠른 질문 버튼</div>
</div>
""")
with gr.Column():
with gr.Row():
quick1 = gr.Button("🕐 사건 당일 어디에 있었나?", size="sm", variant="secondary")
quick2 = gr.Button("💔 Alexander와 어떤 관계였나?", size="sm", variant="secondary")
with gr.Row():
quick3 = gr.Button("👁️ 의심스러운 행동을 본 적 있나?", size="sm", variant="secondary")
quick4 = gr.Button("🤐 숨기고 있는 게 있다면 말해달라", size="sm", variant="secondary")
with gr.Column(scale=1):
gr.HTML("""
<div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
padding: 20px; border-radius: 12px; margin-bottom: 15px;
border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
display: flex; align-items: center; justify-content: center; min-height: 60px;">
<h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);">🤖 용의자 선택</h3>
</div>
""")
suspect_choice = gr.Radio(
choices=[("👩 Elena (아내)", "Elena"), ("🤖 IRIS-01 (가정부 로봇)", "IRIS-01"), ("👨‍🔬 Dr. Chen (개발자)", "Dr. Chen"), ("🖥️ ZEN (보안 AI)", "ZEN")],
value="Elena", label="심문할 용의자", info="각 용의자를 선택하면 해당 캐릭터와 대화할 수 있습니다"
)
with gr.Group():
character_info = gr.HTML(value=game.get_character_info_html('Elena'), label="")
gr.HTML("""
<div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
padding: 20px; border-radius: 12px; margin-bottom: 15px;
border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
display: flex; align-items: center; justify-content: center; min-height: 60px;">
<h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);">🎯 최종 보고서</h3>
</div>
""")
submit_report_btn = gr.Button("📋 최종 보고서 제출", variant="primary", size="lg", visible=True)
report_status = gr.HTML(value=game.get_report_status_html(), label="")
modal_container = gr.HTML(value="", label="", visible=False)
gr.HTML("""
<div style="background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.95));
padding: 20px; border-radius: 12px; margin-bottom: 15px;
border: 2px solid rgba(255, 255, 255, 0.1); text-align: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); backdrop-filter: blur(5px);
display: flex; align-items: center; justify-content: center; min-height: 60px;">
<h3 style="color: #ffffff; margin: 0; font-size: 18px; font-weight: bold;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);">📊 수사 도구</h3>
</div>
""")
with gr.Column():
case_summary_btn = gr.Button("📋 수사 현황 보기", variant="secondary", size="lg")
case_summary_output = gr.Textbox(label="📊 수사 리포트", lines=14, interactive=False, visible=False)
clear_btn = gr.Button("🔄 수사 초기화", variant="stop", size="lg")
# 이벤트 핸들러
send_btn.click(interrogate_and_update_info, inputs=[message_input, suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
message_input.submit(interrogate_and_update_info, inputs=[message_input, suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
def send_quick_question(question, suspect):
return interrogate_and_update_info(question, suspect)
quick1.click(lambda s: send_quick_question("사건 당일 어디에 있었나요?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
quick2.click(lambda s: send_quick_question("Alexander와 어떤 관계였나요?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
quick3.click(lambda s: send_quick_question("의심스러운 행동을 본 적 있나요?", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
quick4.click(lambda s: send_quick_question("숨기고 있는 게 있다면 말해달라", s), inputs=[suspect_choice], outputs=[chat_display, message_input, interrogation_info, report_status])
suspect_choice.change(update_character_info_and_display, inputs=[suspect_choice], outputs=[character_info, interrogation_info, chat_display])
submit_report_btn.click(game.get_report_modal_html, outputs=[modal_container, modal_container])
case_summary_btn.click(lambda: (game.get_case_summary(), gr.update(visible=True)), outputs=[case_summary_output, case_summary_output])
clear_btn.click(clear_game, outputs=[chat_display, message_input, interrogation_info, report_status])
# 인터페이스 실행
if __name__ == "__main__":
demo.launch(share=True, debug=True)