File size: 7,865 Bytes
0a47069
3eb99bc
97627cd
b5ff045
9009e60
 
 
93a860a
d57920f
 
 
 
 
 
 
93a860a
eb96cb9
93a860a
eb96cb9
 
d57920f
1a6fb12
93a860a
97627cd
eb96cb9
 
 
 
d57920f
146c098
3eb99bc
1a6fb12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97627cd
3eb99bc
d57920f
3eb99bc
 
93a860a
d57920f
 
 
 
cf1ebab
9009e60
 
8411a77
 
9009e60
 
 
 
 
 
cf1ebab
 
 
 
9f97a01
 
9009e60
 
8411a77
9f97a01
9009e60
 
 
 
8411a77
9009e60
d57920f
 
 
b5ff045
9f97a01
 
 
 
2fe1b55
6b7a4d5
2fe1b55
 
6b7a4d5
 
 
9009e60
6b7a4d5
 
 
2fe1b55
9009e60
2fe1b55
9f97a01
 
 
 
2fe1b55
6b7a4d5
9f97a01
6b7a4d5
 
 
 
 
 
 
2fe1b55
9f97a01
2fe1b55
9f97a01
2fe1b55
 
b5ff045
 
 
 
2fe1b55
b5ff045
 
2fe1b55
b5ff045
 
9009e60
b5ff045
 
d57920f
9f97a01
9009e60
b5ff045
9009e60
 
 
 
b5ff045
a88e1d8
9009e60
 
 
b5ff045
798df17
 
 
8750468
798df17
 
2fe1b55
 
c08ff6f
2fe1b55
 
798df17
 
 
 
 
 
 
 
 
b5ff045
798df17
b5ff045
9009e60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 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