fix: mine_resource 채굴 로직 개선 및 JSON 파싱 안정성 향상
- 광석 위치로 이동 후 실제 자원 존재 여부를 재확인하여 실패한 타일을 제외하는 로직 추가 - JSON 파싱 시 중괄호 및 대괄호 균형을 추적하고, 잘린 응답 복구 로직을 개선하여 안정성 향상 - README.md에 변경 사항 및 기능 설명 추가
This commit is contained in:
@@ -113,5 +113,7 @@ planner.set_goal(
|
|||||||
- 순수 플레이이므로 **걷기, 채굴, 제작에 실제 시간이 소요**됩니다
|
- 순수 플레이이므로 **걷기, 채굴, 제작에 실제 시간이 소요**됩니다
|
||||||
- AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다
|
- AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다
|
||||||
- `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다
|
- `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다
|
||||||
|
- `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다.
|
||||||
- `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다.
|
- `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다.
|
||||||
- 또한 채굴 시작(`mining_state`) 좌표는 정수 타일이 아니라, Lua가 찾은 실제 자원 엔티티의 `e.position`(정확 실수 좌표)을 사용해 “플레이어가 타일 위에 있는데도 즉시 채굴 감지 실패”를 줄입니다.
|
- 또한 채굴 시작(`mining_state`) 좌표는 정수 타일이 아니라, Lua가 찾은 실제 자원 엔티티의 `e.position`(정확 실수 좌표)을 사용해 “플레이어가 타일 위에 있는데도 즉시 채굴 감지 실패”를 줄입니다.
|
||||||
|
- `mine_resource`는 move 후 `p.position` 근처(반경 1.2)에서 실제로 해당 광물 엔티티가 있는지 재확인하고, 없으면 채굴을 시도하지 않고 다음 후보로 넘어갑니다.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -192,20 +192,36 @@ rcon.print("ALL_EXCLUDED")
|
|||||||
return False, f"좌표 파싱 실패: {find_result}"
|
return False, f"좌표 파싱 실패: {find_result}"
|
||||||
|
|
||||||
# 2. 광석 위치로 걸어가기
|
# 2. 광석 위치로 걸어가기
|
||||||
print(f" [채굴] 광석({ox},{oy})으로 이동... (시도 {round_num+1}, 제외: {len(failed_positions)}개)")
|
print(f" [채굴] 광석({mine_x:.2f},{mine_y:.2f})으로 이동... (시도 {round_num+1}, 제외: {len(failed_positions)}개)")
|
||||||
ok, msg = self.move(ox, oy)
|
ok, msg = self.move(mine_x, mine_y)
|
||||||
if not ok:
|
if not ok:
|
||||||
print(f" [채굴] 이동 실패: {msg}")
|
print(f" [채굴] 이동 실패: {msg}")
|
||||||
failed_positions.add((ox, oy))
|
failed_positions.add((ox, oy))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 2.5. 이동 후 실제로 해당 광석이 있는지 재확인
|
||||||
|
# Factorio는 자원이 "타일"이 아니라 "엔티티"라서, 좌표 미세오차/경계로 인해
|
||||||
|
# move 후에도 p.position 근처에 자원이 없으면 채굴을 시도하지 않고 다음 후보로 넘어간다.
|
||||||
|
near_lua = P + f"""
|
||||||
|
local res = p.surface.find_entities_filtered{{position = p.position, radius = 1.2, name = "{ore}"}}
|
||||||
|
if res and #res > 0 then rcon.print("YES") else rcon.print("NO") end
|
||||||
|
"""
|
||||||
|
has_ore = self.rcon.lua(near_lua).strip() == "YES"
|
||||||
|
if not has_ore:
|
||||||
|
failed_positions.add((ox, oy))
|
||||||
|
continue
|
||||||
|
|
||||||
# 3. 현재 위치에서 채굴 시도
|
# 3. 현재 위치에서 채굴 시도
|
||||||
stall_count = 0
|
stall_count = 0
|
||||||
last_item = self._get_item_count(ore)
|
last_item = self._get_item_count(ore)
|
||||||
mined_this_tile = False
|
mined_this_tile = False
|
||||||
|
|
||||||
for tick in range(300): # 최대 30초
|
for tick in range(300): # 최대 30초
|
||||||
self.rcon.lua(P + f"p.mining_state = {{mining = true, position = {{{mine_x}, {mine_y}}}}}")
|
# mining_state로 “우클릭 유지”에 가까운 수동 채굴을 시뮬레이션
|
||||||
|
self.rcon.lua(P + f"""
|
||||||
|
p.update_selected_entity({{x = {mine_x}, y = {mine_y}}})
|
||||||
|
p.mining_state = {{mining = true, position = {{x = {mine_x}, y = {mine_y}}}}}
|
||||||
|
""")
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
if tick % 8 == 7:
|
if tick % 8 == 7:
|
||||||
@@ -215,17 +231,29 @@ rcon.print("ALL_EXCLUDED")
|
|||||||
total_mined = current - before_count
|
total_mined = current - before_count
|
||||||
return True, f"{ore} {total_mined}개 채굴 완료"
|
return True, f"{ore} {total_mined}개 채굴 완료"
|
||||||
|
|
||||||
|
# 아이템이 아직 안 늘었더라도, 채굴 진행률이 올라가기 시작하면 “채굴 시작됨”으로 간주
|
||||||
|
prog_raw = self.rcon.lua(P + "rcon.print(tostring(p.character_mining_progress or 0))")
|
||||||
|
try:
|
||||||
|
prog = float(prog_raw)
|
||||||
|
except:
|
||||||
|
prog = 0.0
|
||||||
|
|
||||||
if current > last_item:
|
if current > last_item:
|
||||||
stall_count = 0
|
stall_count = 0
|
||||||
last_item = current
|
last_item = current
|
||||||
mined_this_tile = True
|
mined_this_tile = True
|
||||||
else:
|
else:
|
||||||
stall_count += 1
|
stall_count += 1
|
||||||
|
if prog > 0.02:
|
||||||
|
mined_this_tile = True
|
||||||
|
|
||||||
# 채굴은 "바로 아이템 카운트가 오르지" 않을 수 있어 너무 빨리 포기하지 않도록 완충
|
# 진행률이 0에 가까운데도 오랫동안 아이템이 안 늘면, 그 타일은 접근 불가로 보고 중단
|
||||||
if stall_count >= 5:
|
if stall_count >= 20 and prog <= 0.02:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# mined_this_tile=false인 경우(진행 시작 전)만 stall_count로 종료
|
||||||
|
# mined_this_tile=true인 경우에는 items가 늦게 증가해도 루프 끝까지 기다림
|
||||||
|
|
||||||
self.rcon.lua(P + "p.mining_state = {mining = false}")
|
self.rcon.lua(P + "p.mining_state = {mining = false}")
|
||||||
total_mined = self._get_item_count(ore) - before_count
|
total_mined = self._get_item_count(ore) - before_count
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ class AIPlanner:
|
|||||||
start = text.find("{")
|
start = text.find("{")
|
||||||
if start == -1:
|
if start == -1:
|
||||||
raise ValueError("JSON 파싱 실패 ('{' 없음):\n" + raw[:300])
|
raise ValueError("JSON 파싱 실패 ('{' 없음):\n" + raw[:300])
|
||||||
depth = 0
|
brace_depth = 0
|
||||||
|
bracket_depth = 0
|
||||||
in_string = False
|
in_string = False
|
||||||
escape = False
|
escape = False
|
||||||
end = start
|
end = start
|
||||||
@@ -256,23 +257,36 @@ class AIPlanner:
|
|||||||
if in_string:
|
if in_string:
|
||||||
continue
|
continue
|
||||||
if c == '{':
|
if c == '{':
|
||||||
depth += 1
|
brace_depth += 1
|
||||||
elif c == '}':
|
elif c == '}':
|
||||||
depth -= 1
|
brace_depth -= 1
|
||||||
if depth == 0:
|
elif c == '[':
|
||||||
|
bracket_depth += 1
|
||||||
|
elif c == ']':
|
||||||
|
bracket_depth -= 1
|
||||||
|
|
||||||
|
if brace_depth == 0 and bracket_depth == 0:
|
||||||
|
# 최상위 JSON 객체가 종료된 지점으로 추정
|
||||||
|
if i > start:
|
||||||
end = i + 1
|
end = i + 1
|
||||||
break
|
break
|
||||||
if depth != 0:
|
if brace_depth != 0 or bracket_depth != 0:
|
||||||
partial = text[start:]
|
partial = text[start:]
|
||||||
partial = self._repair_truncated_json(partial)
|
partial = self._repair_truncated_json(partial)
|
||||||
try:
|
try:
|
||||||
return json.loads(partial)
|
return json.loads(partial)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ValueError(f"JSON 파싱 실패 (잘린 응답 복구 불가):\n{raw[:400]}")
|
raise ValueError(f"JSON 파싱 실패 (잘린 응답 복구 불가):\n{raw[:400]}")
|
||||||
|
candidate = text[start:end]
|
||||||
try:
|
try:
|
||||||
return json.loads(text[start:end])
|
return json.loads(candidate)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ValueError(f"JSON 파싱 실패:\n{raw[:400]}")
|
# 중괄호는 맞지만 배열/후행 속성이 잘려 파싱 실패하는 케이스 복구
|
||||||
|
repaired = self._repair_truncated_json(candidate)
|
||||||
|
try:
|
||||||
|
return json.loads(repaired)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError(f"JSON 파싱 실패:\n{raw[:400]}")
|
||||||
|
|
||||||
def _repair_truncated_json(self, text: str) -> str:
|
def _repair_truncated_json(self, text: str) -> str:
|
||||||
if '"actions"' not in text:
|
if '"actions"' not in text:
|
||||||
@@ -284,9 +298,13 @@ class AIPlanner:
|
|||||||
result = text[:last_complete]
|
result = text[:last_complete]
|
||||||
open_brackets = result.count('[') - result.count(']')
|
open_brackets = result.count('[') - result.count(']')
|
||||||
open_braces = result.count('{') - result.count('}')
|
open_braces = result.count('{') - result.count('}')
|
||||||
result += ']' * open_brackets
|
# JSON이 '...,' 로 끝나는 경우를 방지
|
||||||
result += ',"after_this":"계속 진행"'
|
if result.rstrip().endswith(","):
|
||||||
result += '}' * open_braces
|
result = result.rstrip()[:-1]
|
||||||
|
result += ']' * max(0, open_brackets)
|
||||||
|
if '"after_this"' not in result and open_braces > 0:
|
||||||
|
result += ',"after_this":"계속 진행"'
|
||||||
|
result += '}' * max(0, open_braces)
|
||||||
return result
|
return result
|
||||||
return '{"thinking":"응답 잘림","current_goal":"탐색","actions":[],"after_this":"재시도"}'
|
return '{"thinking":"응답 잘림","current_goal":"탐색","actions":[],"after_this":"재시도"}'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user