From 7abdf8713ac2c8d3da17ee1b7eb9bc7fde2a3a42 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Wed, 25 Mar 2026 21:37:15 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20mine=5Fresource=20=EC=B1=84=EA=B5=B4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20JSON=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EC=95=88=EC=A0=95=EC=84=B1=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 광석 위치로 이동 후 실제 자원 존재 여부를 재확인하여 실패한 타일을 제외하는 로직 추가 - JSON 파싱 시 중괄호 및 대괄호 균형을 추적하고, 잘린 응답 복구 로직을 개선하여 안정성 향상 - README.md에 변경 사항 및 기능 설명 추가 --- README.md | 2 ++ __pycache__/action_executor.cpython-311.pyc | Bin 23916 -> 24916 bytes __pycache__/ai_planner.cpython-311.pyc | Bin 16500 -> 17312 bytes action_executor.py | 38 +++++++++++++++++--- ai_planner.py | 38 ++++++++++++++------ 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 66cd9d4..1af3a73 100644 --- a/README.md +++ b/README.md @@ -113,5 +113,7 @@ planner.set_goal( - 순수 플레이이므로 **걷기, 채굴, 제작에 실제 시간이 소요**됩니다 - AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다 - `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다 +- `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다. - `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다. - 또한 채굴 시작(`mining_state`) 좌표는 정수 타일이 아니라, Lua가 찾은 실제 자원 엔티티의 `e.position`(정확 실수 좌표)을 사용해 “플레이어가 타일 위에 있는데도 즉시 채굴 감지 실패”를 줄입니다. +- `mine_resource`는 move 후 `p.position` 근처(반경 1.2)에서 실제로 해당 광물 엔티티가 있는지 재확인하고, 없으면 채굴을 시도하지 않고 다음 후보로 넘어갑니다. diff --git a/__pycache__/action_executor.cpython-311.pyc b/__pycache__/action_executor.cpython-311.pyc index 950cbd752865c8c526bf532df56f6123afda02c3..b4612c159190ea452521bc56be0a4e11dfc76524 100644 GIT binary patch delta 1939 zcmZ`&YfMvD96#sY+tOCct1n6^y|ksYQd+5iAP9=#;e0SDIG8m}$1PZa!fwmNdK>i1 z;u5kT#|);jbuUHDjP9DvnJvDO%`7v5!L1=#=05DpVrGe$xerTrZi@k@yUF?eU%&t3 z{?EM+ufn;TF#U{NE(7?v_S3DvV(X>!2%VjdO%k8SYAFB{fCDjfiBh5qw1=ej79x4U zCHNzMt-wj>6in_(iV<E|U?}g+)v0STIKP<|+kAM)Pn3r_RJFa8om<#8|jOw@gV)7M)4HYO>_|u2*(YCF-Gh!@}v1u z5Y{XY&gDN}FKP%%O#lL>Bx>jcL2{Vv1P36e=W=@~A&X5*?m{2UWmZRZK{=^Mia*py^vgBuxI)pbT2?GG}tdBG8|Td}AbikzXc$+Gi0$-2L6${rGSu(X>B8)g-%bw>gaYBfKoEygJTe-U zA&;|pd!&Nx8VEWD*+4LC4G)CESn072I=c4xSRby>diw)G?D7t>13h@e5Iw-sYi+ik zXII~INO_^@x5)82wvIpT+zug)9t5b3%UhH0J zrSut(f2O-<*3+{xbU*YKHiuy|u>e3AZY35|VkIY~ zo>e>N)J`NR-?ZzA4CH7NfXt)f9&dugGHE1X0gyS=(dL8%XmrN|6aC};vF2HI$(*|6 zXZ5;!>UFc~@;P<+71cd;?a`*VA~yjDxfYd_H}n z@zvk5)fYFai0@R=Mx*FXUd9ePb;nNaaHa87(a4@b ze{QzG50I+99a{O`dXCDR#v%5s0OtfakN#?mz*?Sd`kfHRuqQHw5_Q}7=~pZi{0QCM zNyAT3Vy8CkV;pN_KS7x-)wtRAmMyTAA8WZxU{XVCiRNn@wux8?7HpdU?E+jy@3gLm z!~Bg_r$Ah2tCc5-WQzDLs%fuEJBCDbBPlcB~g9(rnF~yQ{#k8bcAG+N(JpyQgjw+UN)(ik_akK zu_6Hc?&fD$ntn-zEL`u34|G1@8KX4*XqV6wPt7 zDV3LTxkBNAPJ$|-{p`+GOpqv&tMwj3jKjEx^%zBgR_(LEbOki+IaWEst1_J9;zIO6_zUH-$YbY@JVo9zX&c_D7=_?(67@x>bzET*UDUQ#SEEEf-Owp9dn=-4` z+J&k4sT|22ELeR-s}Hu?x*_|-uKgCFeiKocGpvf1w~Y(JypSEsyA%0=VTSY@mCK4d zQK(N8#AH!S=EdZiXo0=$Z@|6wChKMevUzf2-$}l_zV{JID zU5tg8YCuOeG@$#w{-jO|8uK6N~`+|Iv!`! diff --git a/__pycache__/ai_planner.cpython-311.pyc b/__pycache__/ai_planner.cpython-311.pyc index e33b87f2fe217c6c446d29772b9173d8640fc7cf..0487d353731320d379a11cda15d49b89852b653e 100644 GIT binary patch delta 1737 zcmZ`(U1%Fe5Z=Ah-AOu~PWIU+>#uVBQ~xPerNmBb#c> z$#F1us8CAX)TP2}61k*QEi^TRP^E$3heAs7kRM8`GYS!cfcw~|Dl`wq52bsu?ApOy zc4ue5nfYdKnYkAq!R)8d`UA(A0Umdr+>hPgeam{e8_l$tOl}H*t3ZKS^t`POHTr9A z|K9xs@@AEiSe3?$QApK@eta#Ix}cgALNPgXmda4g3L}%c&kV1aCCfOW!!o7N809eP z#Hh83(h8F>D6|Er^vtW+1I~0{E3;uWNczraz$Z%Ce zVZ|Jcjg9DPPk?xry#2U`h9=QI%@!LR(;O=Fw$M(KvkaW*pWd;QD|11!HKy4#Tdb)~ zM`jz+4Mp3wpeI|S*_zT^n%m^L4leV~On8a+=6P@SaPG)bOK-lVcUh=QPt5kE`-)Ci zCOqGo9xB#3(!(o7R?O^JVqJOGWzMKMxa1Awy#X}8M@cQ$HDu~DiHu|JooQ2OBpNVNgdqHT=TiREi@j`)bE(01HH_K(a>#04sWDn;0H2XloJ}^C6CP)r5*j;l| z*>=s|Rj_yEZPmGgi3>to89?k(m8r_%nb?@$ad1&sJhm8FJod8#AMn(~;et4vw+)v` zJz`~LrOfJAz)Io(e)4->;lM6%Zx=sAk@pP{%Ljz;rU)L0ouhz!Xcb1Az{94NLoE4- z5#I6;kDO4~J)MUb>M>)$x~#e|Po&b0SkVWKmtM<%_Y>%sy_0Ym?d`jV{%RA^-EJ$9 zrEpEx3+AomCc72Gx=RWd9J!^G4Y*gBByQRDA>rP;j&t7F$UB99E^AFKv5Mg$lv#zA zsA})&{b^aVV$Jjt6a5mCutB95jsl4|3OYFC>z{3vCvkrHk}eXuxQN9n^i5fs!l#l^ zc`SK4mS{r1^*4u@5+URAJ{7~2jxF-X*t0}M&P~LVC0b4-<=FX>F&dW=lQG;N=)pQUC}Px-bbzHNcjcwd3{=~d8O zba=n#W41ZEOtP*JkUsI?$29R`@xZ v91J`?yo*R~cq&LuYes+5E^mNdqY={}d4tULz?K?zF(pB?%&l zMORPI3W-rsMhJ=#{#MW*{m6mQ5TyFk&zRJYQ3Snrt}>{Xd!F|_&-0uY-g{2+CjL5x zi=Gu0ni0h3)Y8E5z+BNSKl{GRWQqz1-9r>7*`~+Esv8{hZ~Sku#iA0~q8cc#8rl55 z=ANT!0p(~xncg86s)-sDUiZXp)FfLkayqRLN`Nf{wj9`^JZqqam_UgYsU*G!960W! zCME@3;dlsa_PotW1=%)QijXQmWzMrkS^z9GSB)4A;`lKtu=jx~Y-T?KWe)RGjL-{M zTcMiS$A&V{q{e2WgJK!UPBszT3v9Wa^3-^P`!fTWB%G5uuWNoOID^SZBlKZ&mBma8 zO<@bG54$m8XTo(PX3>==;Q%+y&A$s9uwZwL6ZYxAZi8ql&mfW6n|fP{6OIY9Ml_Ok zIJLe>x8_dU^ITeVC#1=U7D<;^B+lKh)jHCyGVO3SZLw?8t;l$UWt-@=jKG_sctb3` z8%fB?(6pF5I5m_K{cECs)#}e~*u*tkO$K2wZO08~(aL_`ksZ8p z75sgD_2mL)wLwNVf4^je21NE zugA4)tbL|de_qNYz;mRCdhO6bA9V|$hD7rw62*yF2I)CDyp{if7jO}|gr|cU xr03au-rD_z)vPz#fp4 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. 현재 위치에서 채굴 시도 stall_count = 0 last_item = self._get_item_count(ore) mined_this_tile = False 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) if tick % 8 == 7: @@ -215,17 +231,29 @@ rcon.print("ALL_EXCLUDED") total_mined = current - before_count 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: stall_count = 0 last_item = current mined_this_tile = True else: stall_count += 1 + if prog > 0.02: + mined_this_tile = True - # 채굴은 "바로 아이템 카운트가 오르지" 않을 수 있어 너무 빨리 포기하지 않도록 완충 - if stall_count >= 5: + # 진행률이 0에 가까운데도 오랫동안 아이템이 안 늘면, 그 타일은 접근 불가로 보고 중단 + if stall_count >= 20 and prog <= 0.02: break + # mined_this_tile=false인 경우(진행 시작 전)만 stall_count로 종료 + # mined_this_tile=true인 경우에는 items가 늦게 증가해도 루프 끝까지 기다림 + self.rcon.lua(P + "p.mining_state = {mining = false}") total_mined = self._get_item_count(ore) - before_count diff --git a/ai_planner.py b/ai_planner.py index 51bd841..476d579 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -238,7 +238,8 @@ class AIPlanner: start = text.find("{") if start == -1: raise ValueError("JSON 파싱 실패 ('{' 없음):\n" + raw[:300]) - depth = 0 + brace_depth = 0 + bracket_depth = 0 in_string = False escape = False end = start @@ -256,23 +257,36 @@ class AIPlanner: if in_string: continue if c == '{': - depth += 1 + brace_depth += 1 elif c == '}': - depth -= 1 - if depth == 0: + brace_depth -= 1 + 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 break - if depth != 0: + if brace_depth != 0 or bracket_depth != 0: partial = text[start:] partial = self._repair_truncated_json(partial) try: return json.loads(partial) except json.JSONDecodeError: raise ValueError(f"JSON 파싱 실패 (잘린 응답 복구 불가):\n{raw[:400]}") + candidate = text[start:end] try: - return json.loads(text[start:end]) + return json.loads(candidate) 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: if '"actions"' not in text: @@ -284,9 +298,13 @@ class AIPlanner: result = text[:last_complete] open_brackets = result.count('[') - result.count(']') open_braces = result.count('{') - result.count('}') - result += ']' * open_brackets - result += ',"after_this":"계속 진행"' - result += '}' * open_braces + # JSON이 '...,' 로 끝나는 경우를 방지 + if result.rstrip().endswith(","): + 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 '{"thinking":"응답 잘림","current_goal":"탐색","actions":[],"after_this":"재시도"}'