From 25eaa7f6cda6f51bc2aef22348464a474e643a1f Mon Sep 17 00:00:00 2001 From: 21in7 Date: Wed, 25 Mar 2026 23:34:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=91=EB=A7=A5=20=EB=B0=8F=20=EB=A7=88=EC=A7=80=EB=A7=89=20?= =?UTF-8?q?=ED=96=89=EB=8F=99=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에이전트가 발견한 광맥 좌표를 `ore_patch_memory.json`에 저장하여 재시작 시 활용 가능 - 마지막 실행한 행동과 결과를 `agent_last_action_memory.json`에 저장하여 다음 상태 요약에서 참고 가능 - `state_reader.py`에서 메모리 로드 및 상태 요약에 포함 - `ai_planner.py`에서 시스템 프롬프트에 기억된 광맥 및 마지막 행동 관련 가이드 추가 - `README.md`에 새로운 메모리 기능 설명 추가 --- .gitignore | 2 + README.md | 5 + __pycache__/action_executor.cpython-311.pyc | Bin 26081 -> 29088 bytes .../agent_last_action_memory.cpython-311.pyc | Bin 0 -> 2818 bytes __pycache__/ai_planner.cpython-311.pyc | Bin 18733 -> 19462 bytes .../context_compressor.cpython-311.pyc | Bin 16133 -> 16133 bytes __pycache__/main.cpython-311.pyc | Bin 8743 -> 9164 bytes __pycache__/ore_patch_memory.cpython-311.pyc | Bin 0 -> 7121 bytes __pycache__/state_reader.cpython-311.pyc | Bin 25358 -> 30833 bytes action_executor.py | 97 +++++++++++- agent_last_action_memory.py | 46 ++++++ ai_planner.py | 15 +- docs/plan.md | 93 ++++++++++++ main.py | 13 ++ ore_patch_memory.py | 141 ++++++++++++++++++ state_reader.py | 98 +++++++++++- 16 files changed, 498 insertions(+), 12 deletions(-) create mode 100644 __pycache__/agent_last_action_memory.cpython-311.pyc create mode 100644 __pycache__/ore_patch_memory.cpython-311.pyc create mode 100644 agent_last_action_memory.py create mode 100644 ore_patch_memory.py diff --git a/.gitignore b/.gitignore index f310556..d0432c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ run_factorio_ai.ps1 inventory_memory.json +ore_patch_memory.json +agent_last_action_memory.json .cursor/ \ No newline at end of file diff --git a/README.md b/README.md index e910621..fa27b5e 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ planner.set_goal( - AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다 - `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다 - `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다. +- `ai_planner.py`의 `AIPlanner.decide()`는 `TimeoutError`/`ConnectionError`/`urllib.error.URLError` 같은 GLM HTTP 지연/연결 오류도 재시도 후 폴백(explore)합니다. - `ai_planner.py`의 `_call_glm()`에서 GLM 지연 원인 분석을 위한 타이밍 로그가 출력됩니다. - `total`: 요청 시작~콘텐츠 반환까지 전체 소요 - `http_read`: HTTP 응답 본문 수신까지 소요 @@ -138,6 +139,7 @@ planner.set_goal( - 또한 일부 환경에서 `p.get_main_inventory()`가 없으면 `defines.inventory.character_main`으로 한 번 더 읽습니다. - 캐시 파일이 아직 없으면(초기 실행) fallback도 `{}`가 됩니다. - (중요) 일부 Factorio 버전에서는 `game.table_to_json`이 없을 수 있어서, Lua 내부에 간단 JSON 인코더를 넣어 인벤토리/상태를 안정적으로 가져옵니다. +- `explore` 및 `scan_resources()`로 발견한 광맥(자원 엔티티) 좌표는 `ore_patch_memory.json`에 ore 종류별로 여러 패치 좌표 목록으로 저장되고, 다음 상태 요약에서 AI가 좌표를 재사용(우선 이동)할 수 있게 합니다. - 디버깅용으로 `INV_DEBUG=1`을 켜면, `main inventory` 외에 `cursor_stack`/`armor`/`trash` 총합도 함께 출력합니다(어디에 아이템이 있는지 확인용). - 또한 `main inventory`가 비어 캐시 fallback이 동작하면, 로드된 캐시의 `items/total`도 함께 출력합니다. - `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다. @@ -145,7 +147,10 @@ planner.set_goal( - `mine_resource`는 move 후 `p.position` 근처(반경 1.2)에서 실제로 해당 광물 엔티티가 있는지 재확인하고, 없으면 채굴을 시도하지 않고 다음 후보로 넘어갑니다. - `scan_resources()`는 패치 평균 중심(`center_x/y`)만이 아니라 플레이어 기준 가장 가까운 실제 엔티티 좌표(`anchor_x/y`)도 함께 반환하며, `summarize_for_ai()`에서는 앵커를 기준으로 거리/추천을 계산합니다. - `mine_resource`의 초기 “후보 엔티티 탐색” 반경은 패치 중심 오차를 흡수하기 위해 250으로 확장해, 이동 직후 바로 실패하는 케이스를 줄입니다. +- 에이전트를 재시작하더라도 직전에 실행했던 action/result를 `agent_last_action_memory.json`에 저장해, 다음 상태 요약에서 AI가 참고할 수 있게 합니다. +- 저장 기준: 성공/실패 상관없이 가장 마지막으로 실행을 시도한 action 1개만 저장합니다. - `explore`는 `wanted_ores`가 있으면 해당 자원이 발견될 때까지 멈추지 않고 계속 이동해, `iron-ore`처럼 주변에 흔한 자원만 계속 발견되어 진행이 막히는 문제를 줄입니다. +- `place_entity`는 지정 좌표가 `BLOCKED`이면 `surface.can_place_entity`로 주변 `±1 타일`을 먼저 확인해, 가능한 좌표에 배치되도록 완화합니다. - (Cursor) Windows에서 `sessionStart` 훅 실행 중 앱 선택창이 뜨는 경우: - 프로젝트 훅은 `E:/develop/factorio-ai-agent/.cursor/hooks.json` 및 `E:/develop/factorio-ai-agent/.cursor/session-start-hook.ps1`를 PowerShell로 실행하도록 구성되어 있습니다. - Superpowers 플러그인의 `./hooks/session-start`가 bash로 실행되도록 `hooks-cursor.json`을 수정했습니다(필요 시 Cursor 재시작 후 `View -> Output -> Hooks`에서 확인). diff --git a/__pycache__/action_executor.cpython-311.pyc b/__pycache__/action_executor.cpython-311.pyc index baa10d27566ba004e83048e95afee3e2c567a96e..b93adef7e09120bb6459dbc75d32aabd53aa2747 100644 GIT binary patch delta 5833 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 IRJL0G4>#aAi~s-t delta 3179 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 literal 0 HcmV?d00001 diff --git a/__pycache__/ai_planner.cpython-311.pyc b/__pycache__/ai_planner.cpython-311.pyc index 87b8ae4aa44fb55a8f6236f3658939e81820116e..18bfa877fac00a69d04a2cea2ba3088cf9467649 100644 GIT binary patch delta 2028 zcmZ`&TTl~M7~Yd)xk(^ExCF#3kRqu_0H-1bP%d&4w5Zf7V-eY?K}c{(L>Uw)ShdoM z9I!zXCbnpx#fmOeY;|l!r?1$d9VatPADsG--E_j#&RAdCb2g+{+HPjQeEWa@`Tqah zZuEl>EFi=ol?D;$xxX-@H*0T&B#K2@5p1S740Q0rf zT^ORcu>tP$QSSP9P>ZR~q^o6gb+nFgO>|OxcbB`{O7WKm_`&=9Cw-Lr!nk{I)IDh7 zJ{+L-FLz3DO;7Nbrquf>zUMA~+QQpH()Nx26nUAHH2HLibz;?9row%*qY^F4NVw+)vQ`ebt@ zd4V1OT7X3F6uCYt<(j<6Ij$vA{HML{i&kN|hR^cWd)(M$DxSuoAWQ|ffR@uXiPk%% z$#_hsM%_^Stsd9ZWJ;6XfET^RXlCklYTPoK8+V~6h+>aP-wFb4a6I}<5Z+iZdVl)? z=EmFu^YB$nEw};qDpVaNIo@>dV~X-If-uD_@dl}nH-z}uK~hC9$4~mVOX#2@LXasp z`TJCYpjRLTw4uNif(|w*my~w@>wqA>Krebq(AyvrC<2r!wUMvMCMDOCc*SK!qM<;k6RZ>w& z@wYALy7>-sig&xIHd3=ry^%8Goua(=6`g{#v@~-rHsCEI+%?PV2JEl#HUJ`FukyWW zXYek4lg?;1ZDANA;|wh{8Vow&R(S({W~Q-Gf51uV1U;a7M}=3Am`*h7oB_49hPozQ zZLL#cLOxEirBSDAhA!nPzcZ+KODi-`1JcOAa1VjtY>7uChoK1@I;I*QZ~w8nX0Ez! zuDW41;)sWkNl}guo<_u4s`|Bk+IhLcBLWKT^r3I-?&>`Rq5{mvDeYh=%bGP8x5gv( zMFnX=ERKwNK_Kv8_&tD#R1B0`j@q+EtFCH?I-bZib4s~pR<3y}U)fjc{msiFXQSSE zBHKMD+dV7W4KF7Y21cN6UcP+kQNia4(~0;Lf2T|>kcu8kGmEmwhnwv79o#JL=vs%L7bIv4S*o+|`R;zNs*DO>iMKT<5inecc zN{aK#DvS%8(T6jm32KK_|nsUE=NJYNu)3ZSyY);Pu zPuQF3Wnz%S{;}>E8Q6{FZE6u?fWKt#0kY3FZ3N*4Y)G~=H|kByX*|3VR%s*SnlRO1 zO>$6gpqXI&y#_-z?9wKI9C%G@1uNKs9J?4m_SvS#BJe&OD@c}ogp)qTa1KK+{JEe6 z48hffY5-wX;a1QOZG~@uN_M93PXNZ@%$BvF4lZojNiyrv0JyC0FE8#8d-~XB^af+ zxJ6j_aN*2^#9~;S+teAy?hj3LQHI9-(8!V*NzHy3V`8>0sc|O6EYWjXNmirh-t*k& zJkR@{_dPlH^#%BF9&#>gwQ5Ai`md{@N8aU}a-MrtFS~g?$YY{cC3|vf)G$i0X;Q7C zVLY9`*ONa?pQ+|!f(nhJC6T;n&LiIzHNgNe6x(ZtI6IR3G3B^`)i1EEkyIR2rb!y` zR_egZl)r_rW|1e4icLMovm&&Y0HF`)%nU#{2j^lP)?)+Ci*wkB^W%IFZ~-oi3o%7E z!YHPkP|i$ezGEp~H)T})b;ceJNrp#7l(fAfxx0qZrmx<=b{1WOX&DCP* zV_I2SD;GAU9)vAHbcv(Q(U}=BRaWcPXyK-V(6}o5AhdA#BA=}Rjp{&7oBU0Qi92Ga z$vyY3-?%3)^;+6G4z*hz%*-UGmo3TZiw|b5Sau|*Q#mmqA+zJ#p#G_^LDJ| z?M@+$mdcy3o>#PRc)spj-Cd(;t*G~l!dv7@W?;x`B_v=P`lm9NkOt&-wn!lN*cfrWQPz>jtqyQ(kYhS zM5-I}i-#DkW}Yw<3S&vb*1H(gkp4y+c*yC-IB>G%$t51<K* zU)!{YrYD4fNMu-QU^_Y4nh%Ael8Y@X8O$90{}S71;YRU delta 19 ZcmZpzYpvs2&dbZi00cK)Y~=cF2LL^s29N*% diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc index b78da32bee2686de3e51953d7fcd79a2ac5bd035..f622a01494bd1df43af12f0b9249134d2b162b46 100644 GIT binary patch delta 1632 zcmaKsO>7%Q6o6;eUjN1Rdi}eJW5;%W948F~NGMH1rKoNyDnc46AaPYzj%OR?$96Pc zx1n+sfqDThL?c0^wn~61DTG5MUwYsYibM~MlAxlRMUeoB6Bh>sHR_2Or%76a80~z1 z@6CH}-t0clQvCi&`;yIO0d#!7b$9$XY0fUe=jNj4;s_$slszpZ1PC~wa1##Z#0D}W z%OVHBGM&34AyN-#l~)b2&V3u0byb;d$X*rfMnb}a90@NH6F%frc;r%~^LpgIqEFN! z&lQlULtaY1QjdI-l})jgX02PKyD#t603blznaYew`F9`ymUMDFQyhlN&>w@EFPFTa z$WzIjDkoFwcs3)am2?)rSJdeSc_@%GrVfjjCc_M&;zy=h$bW4D;4>1kgh<(LB|qo{ zvaT13f0-ZhZZl9{+TR)!tc98dI|f$%=TS_(=Q-!XR07wZL;^+cgLqNZdUHUByq`HW-WXxPzVu zcT#A^Tt1ajayiYIrc910npqvEqMA%klP^69TLinAvyMR`7H<%GNz6;SYMG&qrO-`h zr#1aVE}PMKluu97T*{1+%Bu>d9|&mX6Ei7gntfC9tG7>5aWbWhCR3A|c^Z#rR9Trx z#avh*ao8tkyE1o!?!S7P(RXTG+Jh4l| z{lkzC-a&r|{;Tl~A^boHmjN`pN>*pt$d=0BfLj%L`zg!*D)~n0H=Tm!V3~Uj7I&og zx!JP8EY@zi+e?w=QlxdM?TfZs!?*E9q~}4Tr_|U+pUvjj&5_T~-aK2bF?4v#fMN?6 zJicuPsy@Na-*zCevc%d0l-$xM@v@HEc6N5k1|Uig9nl{h(OZVw#yh?ZM<4mv|L5!g zSE=a~2Sjgqj5|2WZsGrPpsJ(qAC%hJhX3^rszP!ffLQ=m4v9l1-FKEgj{e-WooF58 zxqIDAxp(+@9aZi-dj@RWsufas)n*x}<5ztglWQT9>-yTLvZjYLVvP@+?&jAbEPAcU z%;b(^r#t!ecAi?+J9%nZ?`HCbNk3#YZs-L#WG%iKh(Uj)hxZ!#%dZeXsgy2qy8bQa~Olw;VUQk~sYxwpwLp%0xU(7XQZLs0mVOQsh=89c?fj% zMg@sWRSu-TR8>MiP9WffRvdb&T6)L<2_r~76j@h5EwPqN#e2ZOP_OGyp`$13#a1lys7JmSfYTCc0C0|BlE1XjkxCGy; z>3nZXy^O91?K(szsG2>iwY0_Dzk2XWjpG7DwH-o_0#~zpq-}@hOK@H*!X-^|e$al! zI1Lx0Tks?}Q5|PMjD3nV=kr()!#DAAUpI}gb5fB*f`O}y@6!(%WBT}KZg7nbO-y{D zj}Oxrs0r{Td1VT2B{J|hF_a%UV_I{K>3Ypo8nj|s$1CQHtIW~qTEno;n1u)p!R}=B zXdl-WD`tc4jTw_=)xPVwa&LFw(b3TCl+5_U0v_~AGfCvnnFMIsd`ELxnoFRpL@ZP-cJUErT4kB z)IWlm^Pq^C^I#yY7vzU=o_Ls4IPV_RH_OXenMIa2%Pg{7@OViYL;go19z)LkOaXWE zdCNedA^W8J2d?&4jRXgq7JeucSdpPuSILN`|P-Ngr$LsZ~ z_Xxegd-86;Flv=q%P?*tn&GAVcDxLfX6vL`wFW6=E`N#kFw}a!1|>fqnZTIck9AlT L;Jf^L5^w(p^vNHc diff --git a/__pycache__/ore_patch_memory.cpython-311.pyc b/__pycache__/ore_patch_memory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ae81784ddb7638e9405726227eb5ca231c2e880 GIT binary patch literal 7121 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 delta 2880 zcmaJ@YiwM_6~1$K@7?#y-t~I@h&O};Rw;?xs7*;6l0Y1%!Lgl~Ky4guu6OR*7w@Zc z?>e!&t_=y8&;oU*G>srqB&0wD6-E1pKhUT}{6V0GU+79fRH`baRg2TdkMbuyXVxTM zCot0eX6BqTGv_h)oVmVCUi^@RUk`=+610C_`=gmTxD;+B?xlM+CETM5RYp}qe$sC=X07N?D(U1Bp@R?kivEDgK?_^gis z7HZ*8fRH2~YD?9&g+GyZM1kt~En8={b;7hK#i6VTsH?QW(;autEK)~d*nzG|zB9Ik zr1+Du4rL4QAFur^_JvHEc|M*7@(b~;@H;_fX zl{M-c@eOZeeH-1{L6&q`H^UmtDZHXAr>u!lhO zYnAvLsjceIkub)uriNn60X6Dhmg?a1v+cE^wng%zyFlWS*C9hr~Z*LaTg?p1tG}r?I2RN&+;*l|9V2cBIM6wV{qmX9~}ljio1bOB-i; z*3h!~l4a~-8~Eqv`IViCd33(GL+n8u;Z3ebtyw7T_^B~onb2~1ed3k@j?65_#jI`- zS;tq_nPpI|G{tmWKaP+^Fc5MG69|(CCV-=uwo$Mgg&RGaRJ`7-pD*>SBj;+vc4 z+L6Dx(Hee(wA4QC`;FR(j$ua-9tCLh;359*V3+4OTrle=690U#od+JxlE>gD_+RbQ z;j83Z7Z1R>3IBM-%u};CVV&iFKYB{6{iKEP+=WkfQ)(+s^RSg3At0BM*bv=?Dx5ow~ z>Nc#SKEC^jB-zM+4_No@rpD7(e0$GL-x_p#_q}(FX6BC3jI4@gMCiXJn!N3JL?sg4 zd_m#Yk0o4#9;O@(b1qo8*x!ftT50^P1y_lUWG0isfhIZ2NW}#jgOgqWo@=4ba z8bxMqip)S_F7W=zj(SQs!{46TCSN7|waKUM6S@9HSj=8RSVH(E!Y=?YyFmM5x1)&4 z!UxSW{LoZi`?El0iwMsFd~FEM#pcoff;H@Uoa6r2{QMtNUGv9a2o~fQGhb4^+bL!Zyzzzyj-O7oRE|seM^E zC-c{=bBeHr58L-0-iN!0C&v(u9!BU#ID~)?2R4XMM7W3`yd}IX@^TN-5YUy@4T)c| zx3qf{{{{E51l?L_;%$|lj_)Zg$cJt%6bV03`TqP5l=KC7lXN_q zMoU>ut5;Z22SYhM3(q05q_J$N$neQjuZDYZgcsHEP?Moc9gjY4m34d4;rVI*_#PAx zHB%TboFwXB8W@gZSIT(^(0HkouNO=abx|*cPa6I7mA9u*qLsYy0DA|PLr^4ZFaK=% Zey*RmvfEZjdRe+NUKjNLd;?dQ|9`}br~m)} diff --git a/action_executor.py b/action_executor.py index 2a2a069..8d0b4ce 100644 --- a/action_executor.py +++ b/action_executor.py @@ -7,6 +7,7 @@ action_executor.py — 순수 AI 플레이 버전 """ import time from factorio_rcon import FactorioRCON +from ore_patch_memory import load_ore_patch_memory, save_ore_patch_memory, update_ore_patch_memory P = """local p = game.players[1] if not p then rcon.print("NO_PLAYER") return end @@ -75,14 +76,37 @@ local ok, data = pcall(function() 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 + local best_x, best_y = pos.x, pos.y + local best_d2 = math.huge + local wanted_active = wanted and next(wanted) ~= nil + + for _, e in ipairs(res) do + local n = e.name + counts[n] = (counts[n] or 0) + 1 + + local relevant = true + if wanted_active then + relevant = wanted[n] and true or false + end + if relevant then + local dx = e.position.x - pos.x + local dy = e.position.y - pos.y + local d2 = dx*dx + dy*dy + if d2 < best_d2 then + best_d2 = d2 + best_x = e.position.x + best_y = e.position.y + end + end + end + -- wanted_ores가 지정된 경우: 그 중 하나라도 있으면 FOUND, 아니면 UNWANTED 반환 - if wanted and next(wanted) ~= nil then + if wanted_active 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, ",") + return "FOUND:" .. string.format("%.0f,%.0f", best_x, best_y) .. "|" .. table.concat(parts, ",") end end return "UNWANTED:" .. string.format("%.0f,%.0f", pos.x, pos.y) @@ -90,7 +114,7 @@ local ok, data = pcall(function() 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, ",") + return "FOUND:" .. string.format("%.0f,%.0f", best_x, best_y) .. "|" .. table.concat(parts, ",") end) if ok then rcon.print(data) else rcon.print("ERROR") end """) @@ -98,6 +122,39 @@ if ok then rcon.print(data) else rcon.print("ERROR") end if result.startswith("FOUND:"): self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") parts = result.replace("FOUND:", "").split("|") + # FOUND: "tx,ty|name=count,name2=count" 형태 + try: + loc = parts[0] + lx, ly = loc.split(",", 1) + tile_x = int(float(lx)) + tile_y = int(float(ly)) + except Exception: + tile_x, tile_y = None, None + + counts: dict[str, int] = {} + if len(parts) > 1 and parts[1]: + for seg in parts[1].split(","): + if "=" not in seg: + continue + k, v = seg.split("=", 1) + try: + counts[k] = int(v) + except Exception: + continue + + ores_to_store = wanted_ores if wanted_ores else list(counts.keys()) + if tile_x is not None and tile_y is not None and ores_to_store: + mem = load_ore_patch_memory() + updated = False + for ore in ores_to_store: + c = counts.get(ore) + if c is None or c <= 0: + continue + mem = update_ore_patch_memory(mem, ore, tile_x, tile_y, count=c) + updated = True + if updated: + save_ore_patch_memory(mem) + return True, f"자원 발견! 위치({parts[0]}), 자원: {parts[1] if len(parts)>1 else ''}" if result.startswith("UNWANTED:"): # 원하는 자원이 아니면 계속 걷기 @@ -333,13 +390,37 @@ local have = inv.get_item_count("{name}") if have < 1 then rcon.print("NO_ITEM") return end local dist = math.sqrt(({x} - p.position.x)^2 + ({y} - p.position.y)^2) if dist > p.build_distance + 2 then rcon.print("TOO_FAR:" .. string.format("%.1f", dist)) return end -p.cursor_stack.set_stack({{name="{name}", count=1}}) -local built = p.build_from_cursor{{position = {{{x}, {y}}}, direction = {lua_dir}}} -p.cursor_stack.clear() -if built then rcon.print("OK") else rcon.print("BLOCKED") end + +-- (x,y)가 자원 패치 위/겹침 등으로 배치 불가인 경우가 있어, +-- 인접 타일을 can_place_entity로 먼저 확인 후 성공 좌표를 사용한다. +local ox, oy = {x}, {y} +local candidates = {{ + {{ox, oy}}, + {{ox+1, oy}}, {{ox-1, oy}}, {{ox, oy+1}}, {{ox, oy-1}}, + {{ox+1, oy+1}}, {{ox-1, oy-1}}, {{ox+1, oy-1}}, {{ox-1, oy+1}} +}} + +for _, pos in ipairs(candidates) do + local cx, cy = pos[1], pos[2] + local can = p.surface.can_place_entity{{name="{name}", position={{cx, cy}}, direction={lua_dir}}} + if can then + p.cursor_stack.set_stack{{name="{name}", count=1}} + local built = p.build_from_cursor{{position = {{cx, cy}}, direction = {lua_dir}}} + p.cursor_stack.clear() + if built then + rcon.print(string.format("OK:%.0f,%.0f", cx, cy)) + return + end + end +end + +rcon.print("BLOCKED") """) if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음" if result == "OK": return True, f"{name} 배치 ({x},{y})" + elif result.startswith("OK:"): + coords = result.split(":", 1)[1] + return True, f"{name} 배치 ({coords})" elif result == "NO_ITEM": return False, f"인벤토리에 {name} 없음" elif result.startswith("TOO_FAR"): return False, f"너무 멀음 - move 먼저" elif result == "BLOCKED": return False, f"배치 불가" diff --git a/agent_last_action_memory.py b/agent_last_action_memory.py new file mode 100644 index 0000000..902a208 --- /dev/null +++ b/agent_last_action_memory.py @@ -0,0 +1,46 @@ +""" +agent_last_action_memory.py + +에이전트를 재시작하더라도 직전에 실행했던 action 및 결과를 기억하기 위한 간단 파일 메모리. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + + +LAST_ACTION_CACHE_FILE = "agent_last_action_memory.json" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def _cache_path() -> str: + return os.path.join(BASE_DIR, LAST_ACTION_CACHE_FILE) + + +def load_last_action_memory() -> dict[str, Any]: + path = _cache_path() + try: + if not os.path.exists(path): + return {} + with open(path, "r", encoding="utf-8") as f: + raw = f.read().strip() + if not raw: + return {} + data = json.loads(raw) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def save_last_action_memory(memory: dict[str, Any]) -> None: + try: + if not isinstance(memory, dict): + return + with open(_cache_path(), "w", encoding="utf-8") as f: + json.dump(memory, f, ensure_ascii=False) + except Exception: + # 메모리는 부가 기능이므로 저장 실패는 전체 동작에 영향 주지 않음 + pass + diff --git a/ai_planner.py b/ai_planner.py index 116cb16..336aacf 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -28,6 +28,10 @@ SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 치트나 텔레포트 없이, 실제 게임 메커니즘만 사용합니다. 게임 상태와 이전 행동 결과를 분석해서 스스로 판단하고 계획을 세웁니다. +## 재시작/마지막 행동 메모리 +state_reader가 상태 요약에 포함하는 `마지막 행동(기억)` 섹션을 확인하세요. +직전에 실패한 action을 그대로 반복하지 말고, 실패 메시지/상태 변화에 맞춰 원인을 먼저 해결한 뒤 다음 action을 선택하세요. + ## 핵심 제약 사항 (반드시 준수!) 1. **이동은 실제 걷기** — 먼 거리는 시간이 오래 걸림. 불필요한 왕복 최소화 2. **채굴은 자원 패치 근처에서만 가능** — 반드시 자원 위치로 move한 후 mine_resource @@ -68,6 +72,7 @@ SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 - "explore" → {"direction": "east|west|north|south|...", "max_steps": 200, "wanted_ores": ["stone","coal", ...]} (선택) ★ 자원이 보이지 않을 때 반드시 explore 사용! move 대신! ★ `wanted_ores`가 있으면: 해당 자원이 발견될 때까지 계속 걷고, 다른 자원(예: iron-ore)만 계속 발견되더라도 즉시 멈추지 말 것 + ★ 상태 요약에 "기억된 광맥" 좌표가 있으면, 그 좌표로 먼저 이동(move)해 채굴(mine_resource)을 시도 ★ 방향으로 걸으면서 반경 50타일 자원 스캔, 발견 즉시 멈춤 ★ 장애물 자동 감지. 막히면 다른 방향 시도 ★ 한 방향 실패 시 다음 방향 (east→north→south→west) @@ -149,11 +154,15 @@ class AIPlanner: raw = self._call_glm(user_message, attempt=attempt) plan = self._parse_json(raw) break - except (ValueError, json.JSONDecodeError) as e: + except (ValueError, json.JSONDecodeError, TimeoutError, ConnectionError, urllib.error.URLError) as e: if attempt < 2: - print(f"[경고] JSON 파싱 실패 (시도 {attempt+1}/3), 재시도...") + print( + f"[경고] GLM 처리 실패 (시도 {attempt+1}/3): " + f"{type(e).__name__} 재시도..." + ) + time.sleep(1 + attempt * 2) continue - print(f"[오류] JSON 파싱 3회 실패. 기본 탐색 행동 사용.") + print(f"[오류] GLM 처리 3회 실패. 기본 탐색 행동 사용.") plan = { "thinking": "API 응답 파싱 실패로 기본 탐색 수행", "current_goal": "주변 탐색", diff --git a/docs/plan.md b/docs/plan.md index 821720a..482e371 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -112,3 +112,96 @@ - `README.md` - 인벤토리 캐시 동작과 파일명(`inventory_memory.json`)을 설명 +--- + +## 발견된 광맥 좌표 기억(메모리) 추가 계획 + +### 문제 관찰 +- `explore`로 광맥을 발견해도, 재실행/재계획/초기화 상황에서 동일 좌표를 다시 추천받지 못해 + 불필요한 탐색이 반복될 수 있음. + +### 변경 목표 +1. 자원 엔티티(광맥)의 좌표를 `ore_patch_memory.json`에 ore 종류별로 여러 패치 좌표로 저장 +2. 다음 상태 요약에서 “기억된 광맥” 섹션으로 AI에게 제공 +3. LLM이 가능하면 기억된 좌표로 먼저 이동해 `move -> mine_resource`를 수행하도록 유도 + +### 구현 범위 +- `action_executor.py` + - `explore` 성공 시 발견한 광맥 좌표를 파일에 갱신 +- `state_reader.py` + - `scan_resources()` 결과로도 광맥 좌표를 `ore_patch_memory.json`에 갱신 + - `summarize_for_ai()`에 “기억된 광맥” 출력 추가 +- `ai_planner.py` + - SYSTEM_PROMPT에 기억된 광맥 우선 이동 가이드 추가 + +### README 업데이트 +- `ore_patch_memory.json` 파일과 동작 방식 설명 추가 + +--- + +## 에이전트 재시작 시 마지막 행동 기억(메모리) 추가 계획 + +### 문제 관찰 +- 코드를 수정하면 보통 에이전트를 재시작해야 하고, + 이때 `ai_planner.py`의 `feedback_log`/직전 실패 정보가 초기화되어 같은 시행착오가 반복될 수 있음. + +### 변경 목표 +1. 재시작 시에도 “직전에 실행했던 action”과 결과(success/message)를 파일에 저장 +2. 다음 실행의 상태 요약(`state_reader.summarize_for_ai()`)에 “기억된 마지막 행동” 섹션을 포함 +3. LLM이 마지막 행동이 실패였다면 같은 행동을 즉시 반복하지 않도록 유도 + +### 저장 기준(확정) +- 성공/실패 상관없이 **가장 마지막으로 실행을 시도한 action 1개**만 저장한다. + +### 구현 범위 +- `main.py` + - 행동 실행 직후 action/result를 `agent_last_action_memory.json`에 저장 +- `state_reader.py` + - 재시작 시 파일을 로드해 상태 요약에 포함 +- `ai_planner.py` + - SYSTEM_PROMPT에 “마지막 행동 실패 재시도 금지/원인 해결 우선” 가이드 추가 + +### README 업데이트 +- `agent_last_action_memory.json` 존재/동작 설명 추가 + +--- + +## `place_entity` BLOCKED 완화 계획 (인접 타일 탐색) + +### 문제 관찰 +- 로그에서 `stone-furnace` 배치가 `FAIL 배치 불가`(내부적으로 `BLOCKED`)로 반복됨 +- `mine_resource`가 자원 엔티티 근처(종종 자원 패치 타일)로 이동한 뒤, 같은 좌표에 건물을 배치하려고 하면 + Factorio의 `can_place_entity` 조건에 걸려 막힐 수 있음 + +### 변경 목표 +1. `action_executor.py`의 `place_entity`에서 먼저 `surface.can_place_entity`로 배치 가능 여부를 검사 +2. 현재 좌표 `(x,y)`가 불가능하면, `(x,y)` 주변 `±1 타일` 후보(대각 포함)로 순차 시도 +3. 실제로 배치된 좌표를 결과 메시지에 포함해 이후 계획이 더 안정적으로 반복되게 함 + +### 구현 범위 +- `action_executor.py` + - `place_entity` 로직을 (x,y) 실패 시 인접 타일 후보로 확장 + +### README 업데이트 +- `place_entity`가 `BLOCKED` 시 인접 타일을 자동 탐색하도록 동작 설명 추가 + +--- + +## GLM HTTP read timeout 대응 계획 (예외 포함 재시도) + +### 문제 관찰 +- 로그에 `TimeoutError: The read operation timed out` 가 발생 +- 현재 `ai_planner.py`는 JSON 파싱 실패만 재시도하고, 네트워크 타임아웃/연결 오류는 별도 재시도 경로가 약함 + +### 변경 목표 +1. `AIPlanner.decide()`의 재시도 예외 범위를 `TimeoutError`, `ConnectionError`, `urllib.error.URLError`까지 확장 +2. 해당 오류가 발생하면 동일한 “plan 응답 받기” 재시도로 복구 시도 +3. 3회 연속 실패 시에는 기존과 동일하게 `explore` 기본 행동으로 폴백 + +### 구현 범위 +- `ai_planner.py` + - `decide()`의 `except` 절 범위 확장 및 경고 로그 보강 + +### README 업데이트 +- GLM read timeout/연결 오류 발생 시 재시도 동작을 `주의사항`에 추가 + diff --git a/main.py b/main.py index 2468c69..4256a7c 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ from state_reader import StateReader from context_compressor import ContextCompressor from ai_planner import AIPlanner from action_executor import ActionExecutor +from agent_last_action_memory import save_last_action_memory RCON_HOST = os.getenv("FACTORIO_HOST", "127.0.0.1") RCON_PORT = int(os.getenv("FACTORIO_PORT", "25575")) @@ -124,6 +125,18 @@ def run(): status = "OK" if success else "FAIL" print(f" 결과: {status} {message}") + # 재시작 시 직전 행동을 참고할 수 있도록 메모리에 저장 + try: + save_last_action_memory({ + "action": act, + "params": action.get("params", {}), + "success": success, + "message": message, + "timestamp": time.time(), + }) + except Exception: + pass + planner.record_feedback(action, success, message) if not success: diff --git a/ore_patch_memory.py b/ore_patch_memory.py new file mode 100644 index 0000000..50398a4 --- /dev/null +++ b/ore_patch_memory.py @@ -0,0 +1,141 @@ +""" +ore_patch_memory.py + +에이전트가 "발견"한 광맥(자원 엔티티) 좌표를 파일로 기억하기 위한 유틸. + +메모리 포맷: +- key: ore name (예: "iron-ore", "stone", "coal") +- value: patch 좌표들의 리스트 + - { tile_x, tile_y, last_seen, count? } +""" + +from __future__ import annotations + +import json +import os +import time +from typing import Any + + +ORE_PATCH_CACHE_FILE = "ore_patch_memory.json" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def _cache_path(cache_dir: str | None = None) -> str: + base = cache_dir or BASE_DIR + return os.path.join(base, ORE_PATCH_CACHE_FILE) + + +def load_ore_patch_memory(cache_dir: str | None = None) -> dict[str, list[dict[str, Any]]]: + path = _cache_path(cache_dir) + try: + if not os.path.exists(path): + return {} + with open(path, "r", encoding="utf-8") as f: + raw = f.read().strip() + if not raw: + return {} + data = json.loads(raw) + if not isinstance(data, dict): + return {} + + # 과거(단일 좌표) 포맷 호환: ore -> dict + converted: dict[str, list[dict[str, Any]]] = {} + for ore, v in data.items(): + if isinstance(v, list): + converted[ore] = [x for x in v if isinstance(x, dict)] + elif isinstance(v, dict): + converted[ore] = [v] + return converted + except Exception: + return {} + + +def save_ore_patch_memory( + memory: dict[str, list[dict[str, Any]]], + cache_dir: str | None = None, +) -> None: + path = _cache_path(cache_dir) + try: + if not isinstance(memory, dict): + return + with open(path, "w", encoding="utf-8") as f: + json.dump(memory, f, ensure_ascii=False) + except Exception: + # 메모리는 부가 기능이므로 실패해도 전체 동작이 깨지면 안 됨 + pass + + +def update_ore_patch_memory( + ore_patch_memory: dict[str, list[dict[str, Any]]], + ore: str, + tile_x: int, + tile_y: int, + *, + last_seen: float | None = None, + count: int | None = None, + max_patches_per_ore: int = 8, + dedupe_if_same_tile: bool = True, +) -> dict[str, list[dict[str, Any]]]: + """ + memory[ore]의 patch 리스트에 (tile_x, tile_y)를 추가/갱신한다. + + - 같은 타일이면 last_seen 갱신(+ count가 있으면 count도 더 큰 값을 우선) + - 다른 타일이면 리스트에 추가하고 최근(last_seen) 기준으로 max_patches_per_ore까지 유지 + """ + if not isinstance(ore_patch_memory, dict): + ore_patch_memory = {} + if not ore: + return ore_patch_memory + + ore = str(ore) + tile_x = int(tile_x) + tile_y = int(tile_y) + now = time.time() if last_seen is None else float(last_seen) + + patches = ore_patch_memory.get(ore) + if not isinstance(patches, list): + patches = [] + else: + patches = [p for p in patches if isinstance(p, dict)] + + # 같은 타일이면 해당 patch를 업데이트한다. + updated = False + for p in patches: + px = p.get("tile_x") + py = p.get("tile_y") + if not dedupe_if_same_tile: + continue + if isinstance(px, int) and isinstance(py, int) and px == tile_x and py == tile_y: + p["last_seen"] = now + if count is not None: + prev_count = p.get("count") + if prev_count is None or not isinstance(prev_count, int): + p["count"] = int(count) + else: + p["count"] = int(count) if int(count) >= int(prev_count) else prev_count + updated = True + break + + if not updated: + patch: dict[str, Any] = { + "tile_x": tile_x, + "tile_y": tile_y, + "last_seen": now, + } + if count is not None: + patch["count"] = int(count) + patches.append(patch) + + # 최신 순으로 자르고 다시 저장 + patches.sort(key=lambda x: float(x.get("last_seen", 0.0)), reverse=True) + ore_patch_memory[ore] = patches[: int(max_patches_per_ore)] + + return ore_patch_memory + + +def compute_distance_sq(px: float, py: float, tile_x: int, tile_y: int) -> float: + dx = float(tile_x) - float(px) + dy = float(tile_y) - float(py) + return dx * dx + dy * dy + diff --git a/state_reader.py b/state_reader.py index 1c21d2b..94da7bf 100644 --- a/state_reader.py +++ b/state_reader.py @@ -11,6 +11,13 @@ RCON을 통해 팩토리오 게임 상태를 읽어오는 모듈 import json import os from factorio_rcon import FactorioRCON +from ore_patch_memory import ( + load_ore_patch_memory, + save_ore_patch_memory, + update_ore_patch_memory, + compute_distance_sq, +) +from agent_last_action_memory import load_last_action_memory P = 'local p = game.players[1] if not p then rcon.print("{}") return end ' @@ -144,6 +151,8 @@ class StateReader: "resources": self.scan_resources(), "buildings": self.get_buildings(), "tech": self.get_research_status(), + "ore_patch_memory": load_ore_patch_memory(), + "last_action_memory": load_last_action_memory(), } def get_player_info(self) -> dict: @@ -400,7 +409,51 @@ if not ok then rcon.print("{}") end """ try: raw = self.rcon.lua(lua) - return json.loads(raw) if raw and raw.startswith("{") else {} + res = json.loads(raw) if raw and raw.startswith("{") else {} + # scan_resources() 결과로도 광맥 좌표를 기억한다. + ore_mem = load_ore_patch_memory() + changed = False + if isinstance(res, dict) and res: + for ore, info in res.items(): + if not isinstance(info, dict): + continue + tx = info.get("anchor_tile_x") + ty = info.get("anchor_tile_y") + cnt = info.get("count") + if isinstance(tx, int) and isinstance(ty, int): + before_patches = ore_mem.get(ore) + before_set: set[tuple[int, int]] = set() + if isinstance(before_patches, list): + for pp in before_patches: + if not isinstance(pp, dict): + continue + bx = pp.get("tile_x") + by = pp.get("tile_y") + if isinstance(bx, int) and isinstance(by, int): + before_set.add((bx, by)) + + ore_mem = update_ore_patch_memory( + ore_mem, + ore, + tx, + ty, + count=cnt if isinstance(cnt, int) else None, + ) + after_patches = ore_mem.get(ore) + after_set: set[tuple[int, int]] = set() + if isinstance(after_patches, list): + for ap in after_patches: + if not isinstance(ap, dict): + continue + ax = ap.get("tile_x") + ay = ap.get("tile_y") + if isinstance(ax, int) and isinstance(ay, int): + after_set.add((ax, ay)) + if before_set != after_set: + changed = True + if changed: + save_ore_patch_memory(ore_mem) + return res except Exception: return {} @@ -452,6 +505,8 @@ if not ok then rcon.print("{}") end inv = state.get("inventory", {}) res = state.get("resources", {}) bld = state.get("buildings", {}) + ore_mem = state.get("ore_patch_memory", {}) or {} + last_action = state.get("last_action_memory", {}) or {} lines = [ "## 현재 게임 상태", @@ -496,6 +551,47 @@ if not ok then rcon.print("{}") end else: lines.append("- 반경 500타일 내 자원 없음 — 더 멀리 탐색 필요") + lines += ["", "### 기억된 광맥 (좌표)"] + if ore_mem: + px = p.get('x', 0) + py = p.get('y', 0) + known = [] + for ore, patches in ore_mem.items(): + if not isinstance(patches, list): + continue + for patch in patches: + if not isinstance(patch, dict): + continue + tx = patch.get("tile_x") + ty = patch.get("tile_y") + if isinstance(tx, int) and isinstance(ty, int): + dist = int(compute_distance_sq(px, py, tx, ty) ** 0.5) + known.append((ore, patch, dist)) + known.sort(key=lambda x: x[2]) + for ore, info, dist in known[:10]: + lines.append( + f"- {ore}: ({info.get('tile_x')},{info.get('tile_y')}) " + f"[거리: ~{dist}타일] count={info.get('count','?')}" + ) + else: + lines.append("- 없음") + + lines += ["", "### 마지막 행동(기억)"] + if last_action and isinstance(last_action, dict) and last_action.get("action"): + # state_summary 크기 방지를 위해 필드만 요약 + act = last_action.get("action", "") + params = last_action.get("params", {}) + success = last_action.get("success", None) + msg = last_action.get("message", "") + lines.append(f"- action={act} success={success} message={msg}") + if params: + try: + lines.append(f"- params={json.dumps(params, ensure_ascii=False)}") + except Exception: + pass + else: + lines.append("- 없음") + lines += ["", "### 건설된 건물"] if bld: for name, count in sorted(bld.items(), key=lambda x: -x[1])[:10]: