From 63e9add1dd10c8b41079aeac88909f7f9b43eb10 Mon Sep 17 00:00:00 2001 From: gihyeon Date: Wed, 25 Mar 2026 20:27:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20explore=20=EC=95=A1=EC=85=98=EC=9D=84?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - explore를 최우선 탐색 액션으로 안내 - move는 "좌표를 알 때만" 사용하도록 변경 - fallback 행동도 move→explore로 변경 - 방향별 순서 안내 (east→north→south→west) --- ai_planner.py | 46 +++++++++++----------------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/ai_planner.py b/ai_planner.py index b12782c..c1cf4e9 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -63,9 +63,16 @@ SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 ## 전체 action 목록 -### 이동 (실제 걷기 — 거리에 비례해 시간 소요!) +### 탐색 (★ 자원 없을 때 최우선! 걸으면서 자원 스캔) +- "explore" → {"direction": "east|west|north|south|...", "max_steps": 200} + ★ 자원이 보이지 않을 때 반드시 explore 사용! move 대신! + ★ 방향으로 걸으면서 반경 50타일 자원 스캔, 발견 즉시 멈춤 + ★ 장애물 자동 감지. 막히면 다른 방향 시도 + ★ 한 방향 실패 시 다음 방향 (east→north→south→west) + +### 이동 (자원 좌표를 알 때만 사용) - "move" → {"x": int, "y": int} - 주의: 건설/채굴/삽입 전에 반드시 해당 위치 근처로 move + 주의: 자원/건물의 정확한 좌표를 알 때만 사용. 탐색에는 explore! ### 채굴 (자원 패치 근처에서만 작동) - "mine_resource" → {"ore": "iron-ore", "count": int} @@ -149,7 +156,7 @@ class AIPlanner: "thinking": "API 응답 파싱 실패로 기본 탐색 수행", "current_goal": "주변 탐색", "actions": [ - {"action": "move", "params": {"x": 20, "y": 0}, "reason": "탐색을 위해 이동"}, + {"action": "explore", "params": {"direction": "east", "max_steps": 200}, "reason": "자원 탐색"}, ], "after_this": "자원 발견 후 채굴 시작" } @@ -196,7 +203,7 @@ class AIPlanner: {"role": "user", "content": user_message}, ], "temperature": 0.3, - "max_tokens": 2000, # 1500 → 2000으로 증가 (잘림 방지) + "max_tokens": 2000, }).encode("utf-8") req = urllib.request.Request( @@ -216,38 +223,25 @@ class AIPlanner: raise ConnectionError(f"GLM API 오류 {e.code}: {e.read().decode()}") def _parse_json(self, raw: str) -> dict: - """GLM 응답에서 JSON을 안전하게 추출. 여러 형태 대응.""" text = raw.strip() - - # 1. 태그 제거 if "" in text: text = text.split("")[-1].strip() - - # 2. 마크다운 코드블록 제거 if text.startswith("```"): - # ```json ... ``` 패턴 text = "\n".join( l for l in text.splitlines() if not l.strip().startswith("```") ).strip() - - # 3. 순수 JSON 시도 try: return json.loads(text) except json.JSONDecodeError: pass - - # 4. { } 범위 추출 (중첩 괄호 고려) start = text.find("{") if start == -1: raise ValueError("JSON 파싱 실패 ('{' 없음):\n" + raw[:300]) - - # 중첩 괄호 카운팅으로 정확한 끝 위치 찾기 depth = 0 in_string = False escape = False end = start - for i in range(start, len(text)): c = text[i] if escape: @@ -268,50 +262,32 @@ class AIPlanner: if depth == 0: end = i + 1 break - if depth != 0: - # 괄호가 안 닫힘 — 잘린 응답. 복구 시도 partial = text[start:] - # 잘린 actions 배열 복구 partial = self._repair_truncated_json(partial) try: return json.loads(partial) except json.JSONDecodeError: raise ValueError(f"JSON 파싱 실패 (잘린 응답 복구 불가):\n{raw[:400]}") - try: return json.loads(text[start:end]) except json.JSONDecodeError: raise ValueError(f"JSON 파싱 실패:\n{raw[:400]}") def _repair_truncated_json(self, text: str) -> str: - """잘린 JSON 응답 복구 시도""" - # 마지막 완전한 action 항목까지만 유지 - # actions 배열에서 마지막 완전한 } 를 찾아서 배열 닫기 - - # "actions" 키가 있는지 확인 if '"actions"' not in text: - # actions도 없으면 기본 구조로 대체 return '{"thinking":"응답 잘림","current_goal":"탐색","actions":[],"after_this":"재시도"}' - - # 마지막으로 완전한 action 객체의 } 위치 찾기 last_complete = -1 - # "reason": "..." } 패턴의 마지막 위치 for m in re.finditer(r'"reason"\s*:\s*"[^"]*"\s*\}', text): last_complete = m.end() - if last_complete > 0: - # 그 지점까지 자르고 배열과 객체 닫기 result = text[:last_complete] - # 열린 괄호 세기 open_brackets = result.count('[') - result.count(']') open_braces = result.count('{') - result.count('}') result += ']' * open_brackets - # after_this 추가 result += ',"after_this":"계속 진행"' result += '}' * open_braces return result - return '{"thinking":"응답 잘림","current_goal":"탐색","actions":[],"after_this":"재시도"}' def set_goal(self, goal: str):