diff --git a/action_executor.py b/action_executor.py index 0aefb83..e538582 100644 --- a/action_executor.py +++ b/action_executor.py @@ -1,39 +1,44 @@ """ action_executor.py — 순수 AI 플레이 버전 (치트 없음) -모든 행동이 실제 게임 메커닉을 사용: +모든 행동이 실제 게임 메커니즘을 사용: - 이동: 텔레포트 대신 실제 걷기 (walking_state) - 채굴: 인벤토리 직접 삽입 대신 실제 채굴 (mining_state) - 제작: 무조건 지급 대신 실제 제작 (begin_crafting, 재료 소모) - 건설: create_entity 대신 커서 배치 (build_from_cursor, 거리 제한) + +핵심 변경: game.player → game.players[1] (RCON 호환) """ import time import json from factorio_rcon import FactorioRCON +# 모든 Lua 코드 앞에 붙일 플레이어 참조 (RCON에서 game.player는 nil) +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]: - """ - 행동 실행 후 (성공여부, 메시지) 반환 - AI 피드백 루프에서 사용 - """ act = action.get("action", "") params = action.get("params", {}) handlers = { - "move": self.move, - "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, + "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) @@ -41,22 +46,28 @@ class ActionExecutor: return False, f"알 수 없는 행동: {act}" try: - return handler(**params) + result = handler(**params) + # NO_PLAYER / NO_CHARACTER 에러 공통 처리 + 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 move(self, x: int, y: int) -> tuple[bool, str]: """8방향 walking_state로 실제 걷기. 도착까지 폴링.""" - max_ticks = 400 # 최대 ~40초 (먼 거리 대비) + max_ticks = 400 stuck_count = 0 last_dist = 99999 for _ in range(max_ticks): - result = self.rcon.lua(f""" -local p = game.player + result = self.rcon.lua(P + f""" local tx, ty = {x}, {y} local dx = tx - p.position.x local dy = ty - p.position.y @@ -84,6 +95,9 @@ 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})로 도착" @@ -99,20 +113,19 @@ rcon.print("WALK:" .. string.format("%.1f", dist)) pass if stuck_count > 30: - self.rcon.lua("game.player.walking_state = {walking = false, direction = defines.direction.north}") + 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("game.player.walking_state = {walking = false, direction = defines.direction.north}") + self.rcon.lua(P + "p.walking_state = {walking = false, direction = defines.direction.north}") return False, f"({x},{y})에 도달 못함 (시간 초과, 남은 거리: {last_dist:.0f})" - # ── 자원 채굴 (실제 mining_state) ─────────────────────────── + # ── 자원 채굴 (실제 mining_state) ──────────────────────────────── def mine_resource(self, ore: str = "iron-ore", count: int = 10) -> tuple[bool, str]: - """플레이어 근처 자원을 실제로 채굴. 자원 패치 근처에 있어야 함.""" + """플레이어 근처의 자원을 실제로 채굴.""" # 근처 자원 찾기 - find_result = self.rcon.lua(f""" -local p = game.player + find_result = self.rcon.lua(P + f""" local surface = p.surface local resources = surface.find_entities_filtered{{ position = p.position, radius = 5, name = "{ore}" @@ -129,6 +142,9 @@ else rcon.print("NOT_FOUND") end """) + 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 먼저" @@ -137,30 +153,29 @@ end before_count = self._get_item_count(ore) target_count = before_count + count - max_ticks = count * 40 # 자원당 ~4초 여유 + max_ticks = count * 40 for _ in range(max_ticks): - self.rcon.lua(f""" -game.player.mining_state = {{mining = true, position = {{{rx}, {ry}}}}} + 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("game.player.mining_state = {mining = false}") + self.rcon.lua(P + "p.mining_state = {mining = false}") return True, f"{ore} {current - before_count}개 채굴 완료" - self.rcon.lua("game.player.mining_state = {mining = false}") + 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} 채굴 실패 — 너무 멀거나 자원 고갈" - # ── 아이템 제작 (실제 begin_crafting) ─────────────────────── + # ── 아이템 제작 (실제 begin_crafting) ───────────────────────────── def craft_item(self, item: str, count: int = 1) -> tuple[bool, str]: """실제 제작 큐 사용. 재료가 인벤토리에 있어야 함.""" - result = self.rcon.lua(f""" -local p = game.player + result = self.rcon.lua(P + f""" local recipe = p.force.recipes["{item}"] if not recipe then rcon.print("NO_RECIPE") @@ -178,9 +193,11 @@ 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]) - # 제작 완료 대기 (레시피당 ~0.5~2초) wait_time = min(crafted * 2, 30) time.sleep(wait_time) return True, f"{item} {crafted}개 제작 완료" @@ -192,7 +209,7 @@ end return False, f"{item} 재료 부족 — 필요 재료를 먼저 채굴/제작" return False, f"제작 실패: {result}" - # ── 건물 배치 (커서 + build_from_cursor) ──────────────────── + # ── 건물 배치 (커서 + build_from_cursor) ────────────────────────── def place_entity( self, name: str, @@ -208,8 +225,7 @@ end } lua_dir = dir_map.get(direction, "defines.direction.north") - result = self.rcon.lua(f""" -local p = game.player + result = self.rcon.lua(P + f""" local inv = p.get_main_inventory() -- 인벤토리 확인 @@ -248,6 +264,9 @@ 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.startswith("NO_ITEM"): @@ -261,17 +280,16 @@ end return False, f"({x},{y}) 배치 불가 — 지형 또는 건물 겹침" 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 = 0 failed = 0 - # 경로 좌표 계산 positions = [] if from_x != to_x: step = 1 if from_x < to_x else -1 @@ -287,7 +305,6 @@ end positions.append((to_x, by, direction)) for bx, by, direction in positions: - # 건설 거리 내로 이동 ok, msg = self.move(bx, by) if not ok: failed += 1 @@ -305,16 +322,15 @@ end return True, f"벨트 {placed}개 설치, {failed}개 실패" return True, f"벨트 {placed}개 설치 완료" - # ── 건물에 아이템 삽입 (수동 전달) ────────────────────────── + # ── 건물에 아이템 삽입 (수동 전달) ──────────────────────────────── def insert_to_entity( self, x: int, y: int, item: str = "coal", count: int = 50 ) -> tuple[bool, str]: - """플레이어 인벤토리에서 꺼내 건물에 넣음. 건설 거리 내 필요.""" - result = self.rcon.lua(f""" -local p = game.player + """플레이어 인벤토리에서 꺼내 건물에 넣음.""" + 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:" .. string.format("%.1f", dist)) @@ -368,6 +384,9 @@ end rcon.print(inserted and "OK" or "NOT_FOUND") """) + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): + return False, result or "플레이어 없음" + if result == "OK": return True, f"({x},{y}) 건물에 {item} 삽입 완료" elif result.startswith("TOO_FAR"): @@ -376,10 +395,9 @@ rcon.print(inserted and "OK" or "NOT_FOUND") return False, f"인벤토리에 {item} 없음" return False, f"({x},{y})에 적합한 건물 없음" - # ── 조립기 레시피 설정 ────────────────────────────────────── + # ── 조립기 레시피 설정 ──────────────────────────────────────────── def set_recipe(self, x: int, y: int, recipe: str) -> tuple[bool, str]: - result = self.rcon.lua(f""" -local p = game.player + 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") @@ -398,16 +416,19 @@ else rcon.print("NOT_FOUND") end """) + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): + return False, result or "플레이어 없음" + if result == "OK": return True, f"({x},{y}) 레시피: {recipe}" elif result == "TOO_FAR": return False, f"({x},{y})가 너무 멀음 — move 먼저" return False, f"({x},{y})에 조립기 없음" - # ── 연구 시작 ─────────────────────────────────────────────── + # ── 연구 시작 ──────────────────────────────────────────────────── def start_research(self, tech: str = "automation") -> tuple[bool, str]: - result = self.rcon.lua(f""" -local force = game.player.force + result = self.rcon.lua(P + f""" +local force = p.force local t = force.technologies["{tech}"] if not t then rcon.print("NO_TECH") @@ -419,6 +440,9 @@ else rcon.print("OK") end """) + if not result or result in ("NO_PLAYER", "NO_CHARACTER"): + return False, result or "플레이어 없음" + if result == "OK": return True, f"{tech} 연구 시작" elif result == "ALREADY_DONE": @@ -427,16 +451,15 @@ end 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(f""" -rcon.print(tostring(game.player.get_main_inventory().get_item_count("{item}"))) + result = self.rcon.lua(P + f""" +rcon.print(tostring(p.get_main_inventory().get_item_count("{item}"))) """) try: return int(result)