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