From 8575cf4f760ff81d41b4cbc28ccdeeb13833f4ec Mon Sep 17 00:00:00 2001 From: gihyeon Date: Wed, 25 Mar 2026 20:41:41 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20mine=5Fresource=20=EC=B1=84=EA=B5=B4=20?= =?UTF-8?q?=EC=95=88=20=EB=90=98=EB=A9=B4=20=EB=8B=A4=EB=A5=B8=20=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=9E=90=EB=8F=99=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lua에서 가장 가까운 광석 5개를 거리순으로 반환 - 매 10틱마다 진행 체크 (아이템 수 변화 확인) - 3번 연속 진행 없으면 → 다음 타일로 자동 전환 - 최대 5개 타일 순서대로 시도 - 절벽/장애물 때문에 못 닿는 타일은 빠르게 스킵 --- action_executor.py | 263 +++++++++++++++++++-------------------------- 1 file changed, 112 insertions(+), 151 deletions(-) diff --git a/action_executor.py b/action_executor.py index 5884fc6..e207a16 100644 --- a/action_executor.py +++ b/action_executor.py @@ -2,8 +2,9 @@ action_executor.py — 순수 AI 플레이 버전 (치트 없음) 핵심 변경: -- explore 액션 추가: 방향으로 걸으면서 자원 스캔, 발견 시 즉시 멈춤 -- game.player → game.players[1] (RCON 호환) +- mine_resource: 채굴 진행 안 되면 다른 광석 타일로 자동 전환 +- Lua에서 가장 가까운 광석 찾기 (거리순 정렬) +- 3번까지 다른 타일 시도 """ import time import json @@ -23,137 +24,82 @@ class ActionExecutor: def execute(self, action: dict) -> tuple[bool, str]: act = action.get("action", "") params = action.get("params", {}) - handlers = { - "move": self.move, - "explore": self.explore, - "mine_resource": self.mine_resource, - "craft_item": self.craft_item, - "place_entity": self.place_entity, - "place_belt_line": self.place_belt_line, - "insert_to_entity": self.insert_to_entity, - "set_recipe": self.set_recipe, - "start_research": self.start_research, - "wait": self.wait, + "move": self.move, "explore": self.explore, + "mine_resource": self.mine_resource, "craft_item": self.craft_item, + "place_entity": self.place_entity, "place_belt_line": self.place_belt_line, + "insert_to_entity": self.insert_to_entity, "set_recipe": self.set_recipe, + "start_research": self.start_research, "wait": self.wait, } - handler = handlers.get(act) if not handler: return False, f"알 수 없는 행동: {act}" - try: result = handler(**params) if isinstance(result, tuple) and isinstance(result[1], str): - if "NO_PLAYER" in result[1]: - return False, "서버에 접속한 플레이어가 없습니다." - if "NO_CHARACTER" in result[1]: - return False, "플레이어 캐릭터가 없습니다." + if "NO_PLAYER" in result[1]: return False, "서버에 접속한 플레이어가 없습니다." + if "NO_CHARACTER" in result[1]: return False, "플레이어 캐릭터가 없습니다." return result - except TypeError as e: - return False, f"파라미터 오류: {e}" - except Exception as e: - return False, f"실행 오류: {e}" + except TypeError as e: return False, f"파라미터 오류: {e}" + except Exception as e: return False, f"실행 오류: {e}" - # ── 탐색 (걸으면서 자원 스캔) ──────────────────────────────────── + # ── 탐색 ───────────────────────────────────────────────────────── def explore(self, direction: str = "east", max_steps: int = 200) -> tuple[bool, str]: - """방향으로 걸으면서 매 20틱마다 반경 50에서 자원을 스캔. - 자원 발견 시 즉시 멈추고 위치 + 자원 정보 반환.""" - dir_map = { - "north": "defines.direction.north", - "south": "defines.direction.south", - "east": "defines.direction.east", - "west": "defines.direction.west", - "northeast": "defines.direction.northeast", - "northwest": "defines.direction.northwest", - "southeast": "defines.direction.southeast", - "southwest": "defines.direction.southwest", + "north": "defines.direction.north", "south": "defines.direction.south", + "east": "defines.direction.east", "west": "defines.direction.west", + "northeast": "defines.direction.northeast", "northwest": "defines.direction.northwest", + "southeast": "defines.direction.southeast", "southwest": "defines.direction.southwest", } lua_dir = dir_map.get(direction, "defines.direction.east") - - # 걷기 시작 self.rcon.lua(P + f"p.walking_state = {{walking = true, direction = {lua_dir}}}") - - scan_interval = 20 # 매 20틱(~2초)마다 스캔 - stuck_count = 0 - last_pos = None - + stuck_count, last_pos = 0, None for step in range(max_steps): time.sleep(0.1) - - # 매 scan_interval 틱마다 자원 스캔 - if step % scan_interval == 0: + if step % 20 == 0: result = self.rcon.lua(P + """ local ok, data = pcall(function() - local surface = p.surface local pos = p.position - local resources = surface.find_entities_filtered{position = pos, radius = 50, type = "resource"} - if #resources == 0 then - return "NONE:" .. string.format("%.0f,%.0f", pos.x, pos.y) - end - -- 자원 이름별 카운트 + 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(resources) do - counts[e.name] = (counts[e.name] or 0) + 1 - end + for _, e in ipairs(res) do counts[e.name] = (counts[e.name] or 0) + 1 end local parts = {} - for name, count in pairs(counts) do - parts[#parts+1] = name .. "=" .. count - end + 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) if ok then rcon.print(data) else rcon.print("ERROR") end """) - - if not result or result in ("NO_PLAYER", "NO_CHARACTER"): - return False, result or "플레이어 없음" - + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음" if result.startswith("FOUND:"): - # 멈추기 self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") - # 파싱: "FOUND:x,y|iron-ore=500,coal=200" parts = result.replace("FOUND:", "").split("|") - pos_str = parts[0] - res_str = parts[1] if len(parts) > 1 else "" - return True, f"자원 발견! 위치({pos_str}), 자원: {res_str}" - + return True, f"자원 발견! 위치({parts[0]}), 자원: {parts[1] if len(parts)>1 else ''}" if result.startswith("NONE:"): pos_str = result.replace("NONE:", "") - # stuck 체크 - if last_pos == pos_str: - stuck_count += 1 - else: - stuck_count = 0 + if last_pos == pos_str: stuck_count += 1 + else: stuck_count = 0 last_pos = pos_str - if stuck_count >= 3: self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") return False, f"탐색 중 장애물에 막힘 위치({pos_str}) — 다른 방향 시도" - - # 시간 초과 self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") - return False, f"{direction} 방향 {max_steps}틱 탐색했으나 자원 미발견 — 다른 방향 시도" + return False, f"{direction} 방향 탐색 자원 미발견 — 다른 방향 시도" - # ── 이동 (실제 걷기) ───────────────────────────────────────────── + # ── 이동 ───────────────────────────────────────────────────────── def move(self, x: int, y: int) -> tuple[bool, str]: - """8방향 walking_state로 실제 걷기. 도착까지 폴링.""" - max_ticks = 400 - stuck_count = 0 - last_dist = 99999 - + max_ticks, stuck_count, last_dist = 400, 0, 99999 for _ in range(max_ticks): result = self.rcon.lua(P + f""" local tx, ty = {x}, {y} local dx = tx - p.position.x local dy = ty - p.position.y local dist = math.sqrt(dx*dx + dy*dy) - if dist < 1.5 then p.walking_state = {{walking = false, direction = defines.direction.north}} rcon.print("ARRIVED:" .. math.floor(p.position.x) .. "," .. math.floor(p.position.y)) return end - local angle = math.atan2(dy, dx) local dir if angle >= -0.393 and angle < 0.393 then dir = defines.direction.east @@ -163,81 +109,104 @@ elseif angle >= 1.963 and angle < 2.749 then dir = defines.direction.southwest elseif angle >= -1.178 and angle < -0.393 then dir = defines.direction.northeast elseif angle >= -1.963 and angle < -1.178 then dir = defines.direction.north elseif angle >= -2.749 and angle < -1.963 then dir = defines.direction.northwest -else dir = defines.direction.west -end - +else dir = defines.direction.west end p.walking_state = {{walking = true, direction = dir}} rcon.print("WALK:" .. string.format("%.1f", dist)) """) - if not result or result in ("NO_PLAYER", "NO_CHARACTER"): - return False, result or "플레이어 없음" - - if result.startswith("ARRIVED"): - return True, f"({x}, {y})로 도착" - + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음" + if result.startswith("ARRIVED"): return True, f"({x}, {y})로 도착" try: dist = float(result.split(":")[1]) - if abs(dist - last_dist) < 0.3: - stuck_count += 1 - else: - stuck_count = 0 + if abs(dist - last_dist) < 0.3: stuck_count += 1 + else: stuck_count = 0 last_dist = dist - except (ValueError, IndexError): - pass - + except: pass if stuck_count > 30: self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") - return False, f"({x},{y}) 이동 중 장애물에 막힘 (남은 거리: {last_dist:.0f})" - + return False, f"({x},{y}) 장애물에 막힘 (남은 거리: {last_dist:.0f})" time.sleep(0.1) - self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") - return False, f"({x},{y})에 도달 못함 (시간 초과, 남은 거리: {last_dist:.0f})" + return False, f"({x},{y}) 시간 초과 (남은 거리: {last_dist:.0f})" - # ── 자원 채굴 ──────────────────────────────────────────────────── + # ── 채굴 (진행 안 되면 다른 타일 시도) ──────────────────────────── def mine_resource(self, ore: str = "iron-ore", count: int = 10) -> tuple[bool, str]: + """가장 가까운 광석부터 시도. 채굴 안 되면 다른 타일로 자동 전환.""" + before_count = self._get_item_count(ore) + target_count = before_count + count + + # Lua에서 가장 가까운 광석 5개 좌표를 거리순으로 반환 find_result = self.rcon.lua(P + f""" local surface = p.surface -local resources = surface.find_entities_filtered{{ - position = p.position, radius = 5, name = "{ore}" -}} -if #resources == 0 then - resources = surface.find_entities_filtered{{ - position = p.position, radius = 15, name = "{ore}" - }} -end -if #resources > 0 then - local r = resources[1] - rcon.print("FOUND:" .. string.format("%.1f,%.1f", r.position.x, r.position.y)) -else - rcon.print("NOT_FOUND") +local pos = p.position +local resources = surface.find_entities_filtered{{position = pos, radius = 20, name = "{ore}"}} +if #resources == 0 then rcon.print("NOT_FOUND") return end +-- 거리순 정렬 +table.sort(resources, function(a, b) + local da = (a.position.x - pos.x)^2 + (a.position.y - pos.y)^2 + local db = (b.position.x - pos.x)^2 + (b.position.y - pos.y)^2 + return da < db +end) +-- 가장 가까운 5개 반환 +local parts = {{}} +for i = 1, math.min(5, #resources) do + local r = resources[i] + parts[#parts+1] = string.format("%.1f,%.1f", r.position.x, r.position.y) end +rcon.print("FOUND:" .. table.concat(parts, ";")) """) if not find_result or find_result in ("NO_PLAYER", "NO_CHARACTER"): return False, find_result or "플레이어 없음" if find_result == "NOT_FOUND": - return False, f"근처에 {ore} 없음 — 자원 패치로 move 먼저" + return False, f"반경 20 내 {ore} 없음 — explore로 자원 찾기" - parts = find_result.replace("FOUND:", "").split(",") - rx, ry = float(parts[0]), float(parts[1]) + # 여러 광석 좌표 파싱 + coords_str = find_result.replace("FOUND:", "") + coord_list = [] + for c in coords_str.split(";"): + parts = c.split(",") + if len(parts) == 2: + coord_list.append((float(parts[0]), float(parts[1]))) - before_count = self._get_item_count(ore) - target_count = before_count + count - max_ticks = count * 40 + if not coord_list: + return False, f"{ore} 좌표 파싱 실패" - for _ in range(max_ticks): - self.rcon.lua(P + f"p.mining_state = {{mining = true, position = {{{rx}, {ry}}}}}") - time.sleep(0.1) - current = self._get_item_count(ore) - if current >= target_count: - self.rcon.lua(P + "p.mining_state = {mining = false}") - return True, f"{ore} {current - before_count}개 채굴 완료" + # 각 좌표를 순서대로 시도 + total_mined = 0 + for attempt, (rx, ry) in enumerate(coord_list): + stall_count = 0 + last_item_count = self._get_item_count(ore) - self.rcon.lua(P + "p.mining_state = {mining = false}") - mined = self._get_item_count(ore) - before_count - if mined > 0: - return True, f"{ore} {mined}개 채굴 (목표 {count}개 중 일부)" - return False, f"{ore} 채굴 실패" + ticks_per_tile = min(count * 20, 200) # 타일당 최대 시도 틱 + for tick in range(ticks_per_tile): + self.rcon.lua(P + f"p.mining_state = {{mining = true, position = {{{rx}, {ry}}}}}") + time.sleep(0.1) + + # 매 10틱마다 진행 체크 + if tick % 10 == 9: + current = self._get_item_count(ore) + if current >= target_count: + self.rcon.lua(P + "p.mining_state = {mining = false}") + total_mined = current - before_count + return True, f"{ore} {total_mined}개 채굴 완료" + + if current == last_item_count: + stall_count += 1 + else: + stall_count = 0 + last_item_count = current + + # 3번 연속 진행 없으면 이 타일 포기 → 다음 타일 + if stall_count >= 3: + print(f" [채굴] 타일({rx:.0f},{ry:.0f}) 접근 불가 — 다음 타일 시도 ({attempt+1}/{len(coord_list)})") + break + + self.rcon.lua(P + "p.mining_state = {mining = false}") + + # 모든 타일 시도 후 + total_mined = self._get_item_count(ore) - before_count + if total_mined > 0: + return True, f"{ore} {total_mined}개 채굴 (목표 {count}개 중 일부 — 접근 가능 타일 부족)" + return False, f"{ore} 채굴 실패 — 모든 근처 타일 접근 불가. 다른 위치로 move 필요" # ── 제작 ───────────────────────────────────────────────────────── def craft_item(self, item: str, count: int = 1) -> tuple[bool, str]: @@ -246,21 +215,16 @@ local recipe = p.force.recipes["{item}"] if not recipe then rcon.print("NO_RECIPE") return end if not recipe.enabled then rcon.print("LOCKED") return end local crafted = p.begin_crafting{{count={count}, recipe="{item}"}} -if crafted > 0 then rcon.print("CRAFTING:" .. crafted) -else rcon.print("NO_INGREDIENTS") end +if crafted > 0 then rcon.print("CRAFTING:" .. crafted) else rcon.print("NO_INGREDIENTS") end """) - if not result or result in ("NO_PLAYER", "NO_CHARACTER"): - return False, result or "플레이어 없음" + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음" if result.startswith("CRAFTING:"): crafted = int(result.split(":")[1]) time.sleep(min(crafted * 2, 30)) return True, f"{item} {crafted}개 제작 완료" - elif result == "NO_RECIPE": - return False, f"{item} 레시피 없음" - elif result == "LOCKED": - return False, f"{item} 레시피 잠김" - elif result == "NO_INGREDIENTS": - return False, f"{item} 재료 부족" + elif result == "NO_RECIPE": return False, f"{item} 레시피 없음" + elif result == "LOCKED": return False, f"{item} 레시피 잠김" + elif result == "NO_INGREDIENTS": return False, f"{item} 재료 부족" return False, f"제작 실패: {result}" # ── 건물 배치 ──────────────────────────────────────────────────── @@ -281,8 +245,7 @@ local built = p.build_from_cursor{{position = {{{x}, {y}}}, direction = {lua_dir if built then p.cursor_stack.clear() rcon.print("OK") else p.cursor_stack.clear() rcon.print("BLOCKED") end """) - if not result or result in ("NO_PLAYER", "NO_CHARACTER"): - return False, result or "플레이어 없음" + 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("NO_ITEM"): return False, f"인벤토리에 {name} 없음" elif result.startswith("TOO_FAR"): return False, f"({x},{y})가 너무 멀음 — move 먼저" @@ -310,7 +273,7 @@ else p.cursor_stack.clear() rcon.print("BLOCKED") end if ok: placed += 1 else: failed += 1 if placed == 0: return False, f"벨트 설치 실패 ({failed}개 실패)" - return True, f"벨트 {placed}개 설치 완료" + (f", {failed}개 실패" if failed else "") + return True, f"벨트 {placed}개 설치" + (f", {failed}개 실패" if failed else "") # ── 아이템 삽입 ────────────────────────────────────────────────── def insert_to_entity(self, x: int, y: int, item: str = "coal", count: int = 50) -> tuple[bool, str]: @@ -381,12 +344,10 @@ else force.research_queue_enabled = true force.add_research("{tech}") rcon.print elif result == "NO_TECH": return False, f"{tech} 기술 없음" return False, result - # ── 대기 ───────────────────────────────────────────────────────── def wait(self, seconds: int = 3) -> tuple[bool, str]: time.sleep(min(seconds, 30)) return True, f"{seconds}초 대기 완료" - # ── 유틸리티 ───────────────────────────────────────────────────── def _get_item_count(self, item: str) -> int: result = self.rcon.lua(P + f'rcon.print(tostring(p.get_main_inventory().get_item_count("{item}")))') try: return int(result)