""" action_executor.py — 순수 AI 플레이 버전 핵심 수정: - mine_resource: 광석 타일로 먼저 WALK → 도착 후 mining_state - 손 채굴 거리 2.7타일 이내에서만 채굴 가능 - 가장 가까운 광석으로 걸어가서 채굴 → 고갈 시 다음 타일로 이동 """ import time from factorio_rcon import FactorioRCON P = """local p = game.players[1] if not p then rcon.print("NO_PLAYER") return end if not p.character then rcon.print("NO_CHARACTER") return end """ class ActionExecutor: def __init__(self, rcon: FactorioRCON): self.rcon = rcon 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, } 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, "캐릭터 없음" return result 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]: 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", } lua_dir = dir_map.get(direction, "defines.direction.east") self.rcon.lua(P + f"p.walking_state = {{walking = true, direction = {lua_dir}}}") 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 + """ 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 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) 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 result.startswith("FOUND:"): 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("NONE:"): pos_str = result.replace("NONE:", "") 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} 방향 자원 미발견 — 다른 방향 시도" # ── 이동 ───────────────────────────────────────────────────────── def move(self, x: int, y: int) -> tuple[bool, str]: 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 elseif angle >= 0.393 and angle < 1.178 then dir = defines.direction.southeast elseif angle >= 1.178 and angle < 1.963 then dir = defines.direction.south 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 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})로 도착" try: dist = float(result.split(":")[1]) if abs(dist - last_dist) < 0.3: stuck_count += 1 else: stuck_count = 0 last_dist = dist 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})" 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})" # ── 채굴 (광석으로 걸어간 뒤 채굴) ────────────────────────────── def mine_resource(self, ore: str = "iron-ore", count: int = 10) -> tuple[bool, str]: """1. 가까운 광석 찾기 → 2. 그 위치로 걸어가기 → 3. 채굴. 한 타일 고갈 시 다음 가까운 타일로 자동 이동.""" before_count = self._get_item_count(ore) target_count = before_count + count total_mined = 0 for round_num in range(10): # 최대 10번 이동+채굴 시도 # 1. 가장 가까운 광석 위치 찾기 find_result = self.rcon.lua(P + f""" local res = p.surface.find_entities_filtered{{position = p.position, radius = 80, name = "{ore}"}} if #res == 0 then rcon.print("NOT_FOUND") return end -- 거리순 정렬해서 가장 가까운 1개 local closest = res[1] local min_dist = 999999 for _, e in ipairs(res) do local d = (e.position.x - p.position.x)^2 + (e.position.y - p.position.y)^2 if d < min_dist then min_dist = d closest = e end end rcon.print(string.format("%.1f,%.1f", closest.position.x, closest.position.y)) """) if not find_result or find_result in ("NO_PLAYER", "NO_CHARACTER", "NOT_FOUND"): if total_mined > 0: return True, f"{ore} {total_mined}개 채굴 (주변 광석 소진)" return False, f"반경 80 내 {ore} 없음 — explore로 다른 광맥 찾기" try: parts = find_result.split(",") ox, oy = float(parts[0]), float(parts[1]) except: return False, f"광석 좌표 파싱 실패: {find_result}" # 2. 광석 위치로 걸어가기 print(f" [채굴] 광석({ox:.0f},{oy:.0f})으로 이동 중...") ok, msg = self.move(int(ox), int(oy)) if not ok: print(f" [채굴] 이동 실패: {msg} — 다음 시도") # 이동 실패해도 현재 위치에서 채굴 시도 pass # 3. 현재 위치에서 반경 3 이내 광석 채굴 (손 채굴 거리) stall_count = 0 last_item = self._get_item_count(ore) for tick in range(300): # 최대 30초 self.rcon.lua(P + f"p.mining_state = {{mining = true, position = {{{ox}, {oy}}}}}") time.sleep(0.1) if tick % 8 == 7: # 매 0.8초마다 체크 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: stall_count = 0 last_item = current else: stall_count += 1 # 2번 연속 진행 없음 → 이 타일 안 닿음, 다음으로 if stall_count >= 2: print(f" [채굴] 타일({ox:.0f},{oy:.0f}) 채굴 불가/고갈 — 다음 타일 이동") break self.rcon.lua(P + "p.mining_state = {mining = false}") total_mined = self._get_item_count(ore) - before_count if total_mined >= count: return True, f"{ore} {total_mined}개 채굴 완료" # 10라운드 후 if total_mined > 0: return True, f"{ore} {total_mined}개 채굴 (목표 {count}개 중 일부)" return False, f"{ore} 채굴 실패 — 접근 가능한 광석 없음" # ── 제작/배치/삽입/레시피/연구/대기 ────────────────────────────── def craft_item(self, item: str, count: int = 1) -> tuple[bool, str]: result = self.rcon.lua(P + f""" 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 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} 재료 부족" return False, f"제작 실패: {result}" def place_entity(self, name: str, x: int, y: int, direction: str = "north") -> tuple[bool, str]: dir_map = {"north": "defines.direction.north", "south": "defines.direction.south", "east": "defines.direction.east", "west": "defines.direction.west"} lua_dir = dir_map.get(direction, "defines.direction.north") result = self.rcon.lua(P + f""" local inv = p.get_main_inventory() 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 """) 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 == "NO_ITEM": return False, f"인벤토리에 {name} 없음" elif result.startswith("TOO_FAR"): return False, f"너무 멀음 — move 먼저" elif result == "BLOCKED": return False, f"배치 불가" return False, f"배치 실패: {result}" def place_belt_line(self, from_x: int, from_y: int, to_x: int, to_y: int) -> tuple[bool, str]: placed, failed, positions = 0, 0, [] if from_x != to_x: step = 1 if from_x < to_x else -1 d = "east" if step == 1 else "west" for bx in range(from_x, to_x + step, step): positions.append((bx, from_y, d)) if from_y != to_y: step = 1 if from_y < to_y else -1 d = "south" if step == 1 else "north" sy = from_y + (step if from_x != to_x else 0) for by in range(sy, to_y + step, step): positions.append((to_x, by, d)) for bx, by, d in positions: ok, _ = self.move(bx, by) if not ok: failed += 1; continue ok, _ = self.place_entity("transport-belt", bx, by, d) if ok: placed += 1 else: failed += 1 if placed == 0: return False, "벨트 설치 실패" 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]: result = self.rcon.lua(P + f""" 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") return end local inv = p.get_main_inventory() local have = inv.get_item_count("{item}") if have < 1 then rcon.print("NO_ITEM") return end local actual = math.min(have, {count}) local entities = p.surface.find_entities_filtered{{position = {{{x},{y}}}, radius = 2}} local inserted = false for _, e in ipairs(entities) do if e.valid and e.burner then local fi = e.burner.inventory if fi then local removed = inv.remove({{name="{item}", count=actual}}) if removed > 0 then fi.insert({{name="{item}", count=removed}}) inserted = true break end end end if not inserted and e.valid then for i = 1, 10 do local ti = e.get_inventory(i) if ti and ti.can_insert({{name="{item}", count=1}}) then local removed = inv.remove({{name="{item}", count=actual}}) if removed > 0 then ti.insert({{name="{item}", count=removed}}) inserted = true end break end end end if inserted then break end end rcon.print(inserted and "OK" or "NOT_FOUND") """) if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, "플레이어 없음" if result == "OK": return True, f"({x},{y})에 {item} 삽입" elif result == "TOO_FAR": return False, "너무 멀음" elif result == "NO_ITEM": return False, f"{item} 없음" return False, "건물 없음" def set_recipe(self, x: int, y: int, recipe: str) -> tuple[bool, str]: result = self.rcon.lua(P + f""" local e = p.surface.find_entities_filtered{{position = {{{x},{y}}}, radius = 2, type = "assembling-machine"}}[1] if e then e.set_recipe("{recipe}") rcon.print("OK") else rcon.print("NOT_FOUND") end """) if result == "OK": return True, f"레시피: {recipe}" return False, "조립기 없음" def start_research(self, tech: str = "automation") -> tuple[bool, str]: result = self.rcon.lua(P + f""" local force = p.force local t = force.technologies["{tech}"] if not t then rcon.print("NO_TECH") elseif t.researched then rcon.print("ALREADY_DONE") else force.research_queue_enabled = true force.add_research("{tech}") rcon.print("OK") end """) if result == "OK": return True, f"{tech} 연구 시작" elif result == "ALREADY_DONE": return True, f"{tech} 완료" return False, result or "실패" 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) except: return 0