""" 협상 환경 시뮬레이터 서비스 """ import random import numpy as np from typing import Dict, Tuple, Optional from app.models.schemas import ScenarioType, PriceZoneType, CardType class NegotiationEnvironment: """협상 환경 시뮬레이터""" def __init__(self): # 문서 기준 가중치 설정 self.scenario_weights = { ScenarioType.A: 1.0, # S_1 = A ScenarioType.D: 0.75, # S_2 = D ScenarioType.C: 0.5, # S_3 = C ScenarioType.B: 0.25 # S_4 = B } self.price_zone_weights = { PriceZoneType.PZ1: 0.1, # P < A (가장 좋은 구간) PriceZoneType.PZ2: 0.5, # A < P < T (중간 구간) PriceZoneType.PZ3: 1.0 # T < P (나쁜 구간) } # 카드별 협상 효과 (시뮬레이션용) self.card_effects = { CardType.C1: {"price_multiplier": 1.2, "success_rate": 0.3}, CardType.C2: {"price_multiplier": 1.1, "success_rate": 0.5}, CardType.C3: {"price_multiplier": 1.0, "success_rate": 0.7}, CardType.C4: {"price_multiplier": 0.9, "success_rate": 0.8} } # 시나리오별 협상 난이도 self.scenario_difficulty = { ScenarioType.A: 1.3, # 가장 어려운 협상 ScenarioType.B: 1.1, # 보통 난이도 ScenarioType.C: 0.95, # 쉬운 협상 ScenarioType.D: 0.85 # 가장 쉬운 협상 } def calculate_reward( self, scenario: ScenarioType, price_zone: PriceZoneType, anchor_price: float, proposed_price: float, is_end: bool ) -> Tuple[float, float]: """ 보상함수 계산: R(s,a) = W × (A/P) + (1-W) × End Args: scenario: 시나리오 타입 price_zone: 가격 구간 anchor_price: 목표가 (A) proposed_price: 제안가 (P) is_end: 협상 종료 여부 Returns: (reward, weight): 보상값과 가중치 """ s_n = self.scenario_weights[scenario] pz_n = self.price_zone_weights[price_zone] # 가중치 계산: W = (S_n + PZ_n) / 2 w = (s_n + pz_n) / 2 # 가격 비율 계산 (0으로 나누기 방지) if proposed_price == 0: price_ratio = float('inf') else: price_ratio = anchor_price / proposed_price # 보상 계산 reward = w * price_ratio + (1 - w) * (1 if is_end else 0) return reward, w def get_price_zone( self, price: float, anchor_price: float, threshold_multiplier: float = 1.2 ) -> PriceZoneType: """ 가격에 따른 구간 결정 Args: price: 제안 가격 anchor_price: 목표가 threshold_multiplier: 임계값 배수 Returns: 가격 구간 """ threshold = anchor_price * threshold_multiplier if price <= anchor_price: return PriceZoneType.PZ1 # 목표가 이하 (좋음) elif price <= threshold: return PriceZoneType.PZ2 # 목표가와 임계값 사이 (보통) else: return PriceZoneType.PZ3 # 임계값 이상 (나쁨) def simulate_opponent_response( self, current_card: CardType, scenario: ScenarioType, anchor_price: float, step: int = 0 ) -> float: """ 상대방 응답 시뮬레이션 Args: current_card: 현재 사용한 카드 scenario: 현재 시나리오 anchor_price: 목표가 step: 현재 협상 단계 Returns: 상대방 제안 가격 """ # 카드 효과 card_effect = self.card_effects[current_card]["price_multiplier"] # 시나리오 난이도 scenario_difficulty = self.scenario_difficulty[scenario] # 협상 진행에 따른 양보 (단계가 늘어날수록 가격 하락) step_discount = 1.0 - (step * 0.05) step_discount = max(step_discount, 0.7) # 최소 30% 할인 # 기본 가격 계산 base_multiplier = card_effect * scenario_difficulty * step_discount # 랜덤 노이즈 추가 (현실적 변동성) noise = np.random.uniform(0.85, 1.15) # 최종 제안 가격 proposed_price = anchor_price * base_multiplier * noise # 최소 가격 보장 (목표가의 70% 이상) min_price = anchor_price * 0.7 proposed_price = max(proposed_price, min_price) return round(proposed_price, 2) def is_negotiation_successful( self, proposed_price: float, anchor_price: float, tolerance: float = 0.05 ) -> bool: """ 협상 성공 여부 판단 Args: proposed_price: 제안 가격 anchor_price: 목표가 tolerance: 허용 오차 (5%) Returns: 협상 성공 여부 """ success_threshold = anchor_price * (1 + tolerance) return proposed_price <= success_threshold def get_all_states(self) -> list[str]: """모든 가능한 상태 목록 반환""" states = ["C0S0P0"] # 초기 상태 for card in CardType: for scenario in ScenarioType: for price_zone in PriceZoneType: state_id = f"{card.value}{scenario.value}{price_zone.value}" states.append(state_id) return states def get_all_actions(self) -> list[CardType]: """모든 가능한 행동 목록 반환""" return list(CardType) def parse_state(self, state_id: str) -> Optional[Dict[str, str]]: """ 상태 ID를 파싱하여 구성 요소 반환 Args: state_id: 상태 ID (예: "C1APZ1") Returns: 상태 구성 요소 딕셔너리 또는 None """ if state_id == "C0S0P0": return {"card": "C0", "scenario": "S0", "price_zone": "P0"} if len(state_id) != 6: # 예: C1APZ1 (6글자) return None try: card = state_id[:2] # C1 scenario = state_id[2] # A price_zone = state_id[3:] # PZ1 # 유효성 검사 if (card in [c.value for c in CardType] and scenario in [s.value for s in ScenarioType] and price_zone in [pz.value for pz in PriceZoneType]): return { "card": card, "scenario": scenario, "price_zone": price_zone } except: pass return None