From 37127f7f4fcf494869ed18b4839a61204eadb118 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Wed, 25 Mar 2026 21:51:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20explore=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=97=90=20wanted=5Fores=20=EB=A7=A4=EA=B0=9C=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - explore 메서드에 wanted_ores 매개변수를 추가하여 특정 자원을 찾을 수 있도록 개선 - 원하는 자원이 발견될 때까지 계속 이동하며, 다른 자원이 발견되더라도 즉시 멈추지 않도록 로직 수정 - 시스템 프롬프트 및 README.md에 변경 사항 반영 --- README.md | 1 + __pycache__/action_executor.cpython-311.pyc | Bin 24916 -> 26092 bytes __pycache__/ai_planner.cpython-311.pyc | Bin 17312 -> 17528 bytes action_executor.py | 34 ++++++++++++++++++-- ai_planner.py | 3 +- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1af3a73..3cf20c2 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,4 @@ planner.set_goal( - `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다. - 또한 채굴 시작(`mining_state`) 좌표는 정수 타일이 아니라, Lua가 찾은 실제 자원 엔티티의 `e.position`(정확 실수 좌표)을 사용해 “플레이어가 타일 위에 있는데도 즉시 채굴 감지 실패”를 줄입니다. - `mine_resource`는 move 후 `p.position` 근처(반경 1.2)에서 실제로 해당 광물 엔티티가 있는지 재확인하고, 없으면 채굴을 시도하지 않고 다음 후보로 넘어갑니다. +- `explore`는 `wanted_ores`가 있으면 해당 자원이 발견될 때까지 멈추지 않고 계속 이동해, `iron-ore`처럼 주변에 흔한 자원만 계속 발견되어 진행이 막히는 문제를 줄입니다. diff --git a/__pycache__/action_executor.cpython-311.pyc b/__pycache__/action_executor.cpython-311.pyc index b4612c159190ea452521bc56be0a4e11dfc76524..6d6d1bc94f73ef562d03c1a56cc23754cbba2252 100644 GIT binary patch delta 3146 zcmZ`*Yfu~472d0rgtS7E#p;2?!;2t*Y-0>K#xS)p2IPWM90Pvwur2E@sbaiRua?#( zQXn0EsAFb=bK{04CLvST#!i|*82-iXHL zB^Eg=@?zpgjjV)N@URM&DiW8ejPUHR8u~LE;Bw2(f!bpq;wRo1ZdoF1i<~0TasDUih zLLJn@ENFnnO9uRsE)PHEa)1thWUgenm<~6XJh?G80-j^ytZZC9RfdVNGKas_xvSV1 z`y7a~Fgvb~vCT~1-guU5dV-gA1u)Dz&KcP-%Bjx#yBTi@dcd+51VuiFm94<%l;VV`ITs)eQgYYiJ72d zNY60Pd0L5J9H1tW@>a6h5b*$AxN$F!-z_q@bAQYr19Zo-BLI5(H9-wvI+vTtnMk!Wp$@&c9mKYbas7HtNF(dRU zg)vGR=!_dgiQEAv}x z9y@qho4jj14u&f#mK-RTU}0uNNM9XEPhDIbza%Vta6A3_Ouex1;Y}fZ`GWA^;>6-l zCl)7fExtS|q~A=WFWp+ac0=$VIMm*}MMz&9T|A$lfCnuW!gi3h)!-~{r7 z`j-O@NKlZ(vvN5NZdimq;l*8o6dDlZABvK~rTqRd5~MA{2_Ym2kw6GV6*^_Z@Cs?k zaWFh6$rBMk+lpvSfX`2R|aXI;Vd^aanYDPu01<7q0i~%nabu(Wf=w-%kSisPE;iJ z-*aqAaCht*GmOSqt3aap8{RoCU#NSv?km3JOTJ{PYkJ2VUp>!Pf5q2-$=Bbk`B*o{ z`{#LoqWMm?^9#?8sV)NQ?_BndL|dZmo=`StD9BBi#*cnucTf1{>?QN|k_@xicnUmZ zHscTMoARgY-+nQ}MXG9c`#q|+COVzrl zj6#v6h(nT$`W0ye(K-EslRO}NA`d~$Ln0~^PL9;{0Pt4w&)d!1mS!UIqe6m1#-@%& ze^+b!zIqg(#5@9jrC)b+ia-~Vv7_O&r`H}Hic>v3y4BrM?3s|{)2UldQdJm#+pq_0 zPkQ$Rfw7iKx(Fm+Zj6AC^8bA6q9d)bMYF)*B1~oPs1X`)BdXo-PSqFCdzekGJ>Hj-x zqI)AHlv;eSt)=xiSr5|xr50u`3F-5fAB;^SD_N4LmD=e8kqEi#N*$1cDwb~Qdt{E@ zr!Yfd7T;~#=s8NJDF9QNQ)g3}=s7Z;L$6Tv^Eh{ZBj`j4NRUpP?3 z+Gx#C0fnmsy3k2{yTem-oyQ_B_{!&wvZ zcxU3=ywRO8J?Uo2+w#z%Dbuxq@nD9b<3uo%tT_%f9{R~jnfn7lIgCQ#6DSy#5KAL& g#j_{9U=GipY-~8G2ks2>zwx0WfAoK}$V7bo2h|EH(*OVf delta 2245 zcmZ`)T}%{L6rMY?yX?-gtiUeI@?&?ADvJs#Dw?Q(2ntrIf};F7a0lHA%l7VKln#w1 zCNR+JajSjOqIP^qP7u5J9*EKWh*{OQC6l;HTdGz#|C5gcajk#5sdJoR=BWoXn;{ z-46)NNi#HOoY2f3v0EfUqo6s@rtqZ;{KUu0JQc(bdVHeh4HFi;OX}dxo9>GYA)&xVu(3lZ#hZ-^phZHuBUTo?tUA3nG^DIXY*o&W}OyBY|pC8 z%zHYY?Xo1UqIu6s9FjGMCR>GI4$V$;2R&Ham`J@tc1<2aTR#6et=!zKKafG^YBC6Z zVMLmK?Jzm91q*0Jcr!Et{?LfnBSIU5^^@N1gy*K`N{S-ttJ6D(Zurg-ek|*?Og9VT zw(zhFsdqpz$jdwu2$!cYHQuXqtA;%h?~NwGr^X^~=EUGqge91f0-e>_r^JmENbQ6N z{$`-ehF!q#1eUqhz&RWie)E@bPj-8LXu(}){xSFPu3vq@Q<1yA!ZBarxRB@SA`?O$ zC&A+EaUpe)Z9+&bLddaCAq+dlbNI&KZM`^WIE7U5FT(3d75^t3A?tWi!Mts?Ce?fm@8dtE5faH+9@K9wm+^;x|yq1&EFvS|E?-e@ezI!$Yw;W{1Hl0xZ!b`i36 zu@_uypZ=h{m5?KP)oPCnRcfgAXSJXs7vLdY2i6ME#QOMP_1i0+E)_d~ek{~1yCL;O zV-dUcinq|UkJoOfBw6|!8@h?S5-f%-ru3=$WKEnbT!s;42+I*x07PsIqgXk@VuV!y zgEgEq*6L#$D+p=kzPeapC0J|OTtGu?*xWRL-=feTmoGoQehTV`C%`)Rg}UQ)@53@8 z*0PH@+h_asnS+lwEz$2_=FAaWO}hncTlrx98d9WRtDltIsL7JJl3(3g1DgKcnzPqD zFNYG3t1)WUp5eD4;$zU^vX%LJdcc675x~;fnVsqWH;8ux;ok)DXK4 z8e-xQ%!k-6I9lk1z_*#CG#= za}GJr*E9!H-UCZ5JIC9aL*bbNjW{fgvm1YMcq7g0$aejg=2DTg@zj>1WR#z5Ssb_y zB-6({oAWvUv89|G)IF^bKFZg&uk)CJl12U{Ki0mQ9OgIM-zC+&tK)Uj%I|k%1!is- zJA<^M75xoQ>x_^S`l`sU7gr>$~$VuS85ZoY!UBDPWv1c9`=9l;EsNM>6 zW*3!M)G7w_;F_3C4imN;M16{$YnJDRHC5x3#WyMSwga%_XOuF+_#GuI{weZFW%v4$ zBKuAm4LGE<7J6O#9UTc$pid{tKHT7ApV% diff --git a/__pycache__/ai_planner.cpython-311.pyc b/__pycache__/ai_planner.cpython-311.pyc index 0487d353731320d379a11cda15d49b89852b653e..bbb6037834c852c750e354333f054645476c72e4 100644 GIT binary patch delta 338 zcmZ3`&iJE)k#9LKFBbz4@Z36_ImdJ(pA<7wz2IgoW?4=H9R;QG#JrN!l=%Fj)M6zo zg=nSXlKi|>B^{;Y{KOn39R)o-z1Ug>jW;a|-ZszGob169T%UlXCE?YE28B2CI^N9L z^LphLD}}dHx4iD&2^5*|X7+?PbGInG-Z1CYrZul8EKqnowdd8&o;S-I6kcs^dDGUb z@M`1sSDP2;D7@}o@_OkGs2+_sGdipkGK=!_bb(INe7(E}rW|IH@{HHLQ(n*A^SZx9 z;mxwnH{Ej-Ua#zUvuz5<=-109D7@O#ym<1P4GI7N006et!)e1bu?`vo0;dqODgzw}0ezEq3{$gn4J--) zbF-iqwg>@$vsWM`1OczJeI(lg0lBkTC+h_P#_QH tuple[bool, str]: + def explore( + self, + direction: str = "east", + max_steps: int = 200, + wanted_ores: list[str] | None = None, + ) -> tuple[bool, str]: dir_map = { "north": "defines.direction.north", "south": "defines.direction.south", "east": "defines.direction.east", "west": "defines.direction.west", @@ -50,17 +55,39 @@ class ActionExecutor: } lua_dir = dir_map.get(direction, "defines.direction.east") self.rcon.lua(P + f"p.walking_state = {{walking = true, direction = {lua_dir}}}") + wanted_ores = wanted_ores or [] + # JSON에서 single string으로 올 수도 있으니 방어 + if isinstance(wanted_ores, str): + wanted_ores = [wanted_ores] + + wanted_lua = "local wanted = {}\n" + for name in wanted_ores: + safe = str(name).replace("\\", "\\\\").replace('"', '\\"') + wanted_lua += f'wanted["{safe}"] = true\n' + stuck_count, last_pos = 0, None for step in range(max_steps): time.sleep(0.1) if step % 20 == 0: - result = self.rcon.lua(P + """ + result = self.rcon.lua(P + wanted_lua + """ local ok, data = pcall(function() local pos = p.position local res = p.surface.find_entities_filtered{position = pos, radius = 50, type = "resource"} if #res == 0 then return "NONE:" .. string.format("%.0f,%.0f", pos.x, pos.y) end local counts = {} for _, e in ipairs(res) do counts[e.name] = (counts[e.name] or 0) + 1 end + -- wanted_ores가 지정된 경우: 그 중 하나라도 있으면 FOUND, 아니면 UNWANTED 반환 + if wanted and next(wanted) ~= nil then + for n, _ in pairs(wanted) do + if counts[n] and counts[n] > 0 then + local parts = {} + for name, count in pairs(counts) do parts[#parts+1] = name .. "=" .. count end + return "FOUND:" .. string.format("%.0f,%.0f", pos.x, pos.y) .. "|" .. table.concat(parts, ",") + end + end + return "UNWANTED:" .. string.format("%.0f,%.0f", pos.x, pos.y) + end + local parts = {} for name, count in pairs(counts) do parts[#parts+1] = name .. "=" .. count end return "FOUND:" .. string.format("%.0f,%.0f", pos.x, pos.y) .. "|" .. table.concat(parts, ",") @@ -72,6 +99,9 @@ if ok then rcon.print(data) else rcon.print("ERROR") end self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") parts = result.replace("FOUND:", "").split("|") return True, f"자원 발견! 위치({parts[0]}), 자원: {parts[1] if len(parts)>1 else ''}" + if result.startswith("UNWANTED:"): + # 원하는 자원이 아니면 계속 걷기 + continue if result.startswith("NONE:"): pos_str = result.replace("NONE:", "") if last_pos == pos_str: stuck_count += 1 diff --git a/ai_planner.py b/ai_planner.py index 476d579..0d62000 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -64,8 +64,9 @@ SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 ## 전체 action 목록 ### 탐색 (★ 자원 없을 때 최우선! 걸으면서 자원 스캔) -- "explore" → {"direction": "east|west|north|south|...", "max_steps": 200} +- "explore" → {"direction": "east|west|north|south|...", "max_steps": 200, "wanted_ores": ["stone","coal", ...]} (선택) ★ 자원이 보이지 않을 때 반드시 explore 사용! move 대신! + ★ `wanted_ores`가 있으면: 해당 자원이 발견될 때까지 계속 걷고, 다른 자원(예: iron-ore)만 계속 발견되더라도 즉시 멈추지 말 것 ★ 방향으로 걸으면서 반경 50타일 자원 스캔, 발견 즉시 멈춤 ★ 장애물 자동 감지. 막히면 다른 방향 시도 ★ 한 방향 실패 시 다음 방향 (east→north→south→west)