feat: explore 액션을 시스템 프롬프트에 추가

- explore를 최우선 탐색 액션으로 안내
- move는 "좌표를 알 때만" 사용하도록 변경
- fallback 행동도 move→explore로 변경
- 방향별 순서 안내 (east→north→south→west)
This commit is contained in:
2026-03-25 20:27:27 +09:00
parent 86af860267
commit 63e9add1dd

View File

@@ -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. <think> 태그 제거
if "<think>" in text:
text = text.split("</think>")[-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):