# portfolio/npc_social_network/npc/npc_manager.py import random from typing import TYPE_CHECKING, List, Optional from .. import simulation_core if TYPE_CHECKING: from .npc_base import NPC def get_korean_postposition(name, first_char, second_char): """이름의 마지막 글자 받침 유무에 따라 올바른 조사를 반환""" if (ord(name[-1]) - 0xAC00) % 28 > 0: return first_char else: return second_char class NPCManager: """모든 NPC를 관리하는 중앙 관리자""" def __init__(self): self.npcs: list['NPC'] = [] self.npc_dict: dict[str, 'NPC'] = {} self.korean_name_to_npc: dict[str, 'NPC'] = {} # 한글 이름 검색용 딕셔너리 self.player_is_active = True # 플레이어 활성화 상태 플래그 def add_npc(self, npc: 'NPC'): """NPC를 매니저에 추가합니다.""" if npc.name not in self.npc_dict: self.npcs.append(npc) self.npc_dict[npc.name] = npc self.korean_name_to_npc[npc.korean_name] = npc npc.manager = self def set_player_active(self, is_active: bool): """플레이어의 활성화 상태를 설정""" self.player_is_active = is_active def get_all_npcs_except_player(self) -> List['NPC']: """플레이어를 제외한 순수 NPC 목록을 반환""" return [npc for npc in self.npcs if npc.name != "player"] def get_interactive_npcs(self) -> List['NPC']: """현재 상화작용이 가능한 NPC 목록을 반환""" if self.player_is_active: return self.npcs # 플레이어가 활성화 상태이면 모두 반환 else: return self.get_all_npcs_except_player() # 비활성화 상태이면 플레이어 제외 def get_npc_by_name(self, name: str) -> Optional['NPC']: """ NPC 영어 ID를 통해서 NPC의 정보를 반환 """ return self.npc_dict.get(name) def get_npc_by_korean_name(self, korean_name: str) -> Optional['NPC']: """NPC 한글 이름을 통해서 NPC의 정보를 반환""" return self.korean_name_to_npc.get(korean_name) def get_random_npc(self, exclude: Optional['NPC']=None) -> Optional['NPC']: """ 특정 NPC를 제외하고 랜덤한 NPC를 선택 수정 필요: 상대 NPC가 랜덤이 아니라, 상호작용할 만한 근거가 있어야한다. 예) 근처에 산다, 특별한 이벤트가 있었다 등 과 같은 이유 """ possible_targets = [n for n in self.npcs if n != exclude] if not possible_targets: return None return random.choice(possible_targets) def all(self) -> List['NPC']: """관리 중인 모든 NPC의 리스트를 반환합니다.""" return self.npcs def initiate_npc_to_npc_interaction(self, initiator: 'NPC', target: 'NPC', time_context: str, topic: Optional[str] = None): """ NPC 간의 상호작용을 시작시키는 함수 - 참여한 NPC를 반환해서 소문을 퍼트릴 '목격자' 생성 - 주제가 있을 경우, 목표 지향적 대화를 생성 """ from ..models.llm_helper import query_llm_with_prompt if len(self.npcs) < 2: return None, None # 상호작용할 NPC가 최소 2명 필요 initiator_postposition = get_korean_postposition(initiator.korean_name, "이", "가") target_postposition = get_korean_postposition(target.korean_name, "에게", "에게") simulation_core.add_log(f"\n---[NPC 상호작용 이벤트]---\n{initiator.korean_name}{initiator_postposition} {target.korean_name}{target_postposition} 상호작용을 시도합니다.") if topic: # 주제가 있는 경우 prompt = f""" # Persona 당신은 '{initiator.korean_name}'입니다. # Context - 당신은 지금 '{target.korean_name}'와 마주쳤습니다. - 당신과 상대의 관계: {initiator.relationships.get_relationship_summary(target.name)} - 대화 주제: "{topic}" # Instruction - 주어진 상황과 대화 주제에 맞춰, 상대방에게 건넬 자연스러운 첫 대사 **한 문장만** 생성하세요. - **절대로** 다른 부가 설명, 이유, 주석 등을 포함하지 마세요. - 당신의 응답은 오직 상대방에게 말할 대사 내용 그 자체여야 합니다. {initiator.korean_name}: """ else: # 주제가 없는 경우 (기존의 가벼운 인사) prompt = f""" # Persona 당신은 "{initiator.korean_name}"입니다. # Context - 당신은 지금 "{target.korean_name}"와 마주쳤습니다. - 당신과 상대의 관계: {initiator.relationships.get_relationship_summary(target.name)} # Instruction - 주어진 상황에 맞춰, 상대방에게 건넬 자연스럽고 가벼운 첫 인사 대사 **한 문장만** 생성하세요. - **절대로** 다른 부가 설명, 이유, 주석 등을 포함하지 마세요. - 당신의 응답은 오직 상대방에게 말할 대사 내용 그 자체여야 합니다. {initiator.korean_name}: """ initial_utterance = query_llm_with_prompt(prompt).strip() # 따옴표도 제거 clean_utterance = initial_utterance.strip('"') # 이름 접두사가 있는지 확인하고 제거 name_prefix = f"{initiator.korean_name}:" if clean_utterance.startswith(name_prefix): clean_utterance = clean_utterance[len(name_prefix):].strip() # 따옴표도 제거 final_utterance = clean_utterance.strip().strip('"') if "[LLM Error]" in final_utterance or not final_utterance: simulation_core.add_log("[Error: {initiator.korean_name} 대화 시작 실패]") print(f"[{initiator.korean_name}] 대화 시작에 실패했습니다.") return None, None, None simulation_core.add_log(f"[{initiator.korean_name}]: {final_utterance}") # 2. Target이 Initiator의 말을 듣고 응답 생성 # generate_dialogue 함수를 재사용하되, target_npc 인자를 전달 response_utterance = target.generate_dialogue( user_input=final_utterance, time_context = time_context, target_npc=initiator ) simulation_core.add_log(f"[{target.korean_name}]: {response_utterance}") # 3. 대화가 끝난 후, 목격자를 선정하여 소문을 생성합니다. potential_witnesses = [npc for npc in self.npcs if npc not in [initiator, target]] if potential_witnesses and random.random() < 0.25: # 25% 확률로 목격자 발생 witness = random.choice(potential_witnesses) initiator_postposition = get_korean_postposition(initiator.korean_name, "이", "가") target_postposition = get_korean_postposition(target.korean_name, "에게", "에게") witness_postposition = get_korean_postposition(witness.korean_name, "이", "가") gossip_content = f"'{initiator.korean_name}'{initiator_postposition} '{target.korean_name}'{target_postposition} '{initial_utterance[:100]}...'라고 말하는 것을 봤다." # 목격자가 '소문'을 기억하도록 함 witness.remember( content=gossip_content, importance=4, emotion="curiosity", memory_type="Gossip", ) # 로그는 simulation_core의 add_log를 직접 호출할 수 없으므로 print로 대체 simulation_core.add_log(f"[목격] {witness.korean_name}{witness_postposition} {initiator.korean_name}와 {target.korean_name}의 대화를 목격함.") return initiator, target, final_utterance