feat: explore 액션 추가 - 걸으면서 자원 스캔, 발견 시 즉시 멈춤
- explore(direction, max_steps): 8방향 걷기 + 매 20틱마다 반경 50 자원 스캔 - 자원 발견 → 즉시 멈추고 위치 + 자원 종류/수량 반환 - 장애물 stuck 감지 → "다른 방향 시도" 메시지 - insert_to_entity: area 대신 position+radius (중괄호 충돌 방지)
This commit is contained in:
@@ -1,20 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
action_executor.py — 순수 AI 플레이 버전 (치트 없음)
|
action_executor.py — 순수 AI 플레이 버전 (치트 없음)
|
||||||
|
|
||||||
모든 행동이 실제 게임 메커니즘을 사용:
|
핵심 변경:
|
||||||
- 이동: 텔레포트 대신 실제 걷기 (walking_state)
|
- explore 액션 추가: 방향으로 걸으면서 자원 스캔, 발견 시 즉시 멈춤
|
||||||
- 채굴: 인벤토리 직접 삽입 대신 실제 채굴 (mining_state)
|
- game.player → game.players[1] (RCON 호환)
|
||||||
- 제작: 무조건 지급 대신 실제 제작 (begin_crafting, 재료 소모)
|
|
||||||
- 건설: 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]
|
P = """local p = game.players[1]
|
||||||
if not p then rcon.print("NO_PLAYER") return end
|
if not p then rcon.print("NO_PLAYER") return end
|
||||||
if not p.character then rcon.print("NO_CHARACTER") return end
|
if not p.character then rcon.print("NO_CHARACTER") return end
|
||||||
@@ -31,6 +26,7 @@ class ActionExecutor:
|
|||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
"move": self.move,
|
"move": self.move,
|
||||||
|
"explore": self.explore,
|
||||||
"mine_resource": self.mine_resource,
|
"mine_resource": self.mine_resource,
|
||||||
"craft_item": self.craft_item,
|
"craft_item": self.craft_item,
|
||||||
"place_entity": self.place_entity,
|
"place_entity": self.place_entity,
|
||||||
@@ -47,18 +43,97 @@ class ActionExecutor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = handler(**params)
|
result = handler(**params)
|
||||||
# NO_PLAYER / NO_CHARACTER 에러 공통 처리
|
|
||||||
if isinstance(result, tuple) and isinstance(result[1], str):
|
if isinstance(result, tuple) and isinstance(result[1], str):
|
||||||
if "NO_PLAYER" in result[1]:
|
if "NO_PLAYER" in result[1]:
|
||||||
return False, "서버에 접속한 플레이어가 없습니다. 팩토리오 클라이언트로 서버에 접속하세요."
|
return False, "서버에 접속한 플레이어가 없습니다."
|
||||||
if "NO_CHARACTER" in result[1]:
|
if "NO_CHARACTER" in result[1]:
|
||||||
return False, "플레이어 캐릭터가 없습니다 (사망했거나 생성 전)."
|
return False, "플레이어 캐릭터가 없습니다."
|
||||||
return result
|
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 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",
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
for step in range(max_steps):
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# 매 scan_interval 틱마다 자원 스캔
|
||||||
|
if step % scan_interval == 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 counts = {}
|
||||||
|
for _, e in ipairs(resources) 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}")
|
||||||
|
# 파싱: "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}"
|
||||||
|
|
||||||
|
if result.startswith("NONE:"):
|
||||||
|
pos_str = result.replace("NONE:", "")
|
||||||
|
# stuck 체크
|
||||||
|
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}틱 탐색했으나 자원 미발견 — 다른 방향 시도"
|
||||||
|
|
||||||
# ── 이동 (실제 걷기) ─────────────────────────────────────────────
|
# ── 이동 (실제 걷기) ─────────────────────────────────────────────
|
||||||
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로 실제 걷기. 도착까지 폴링."""
|
||||||
@@ -79,7 +154,6 @@ if dist < 1.5 then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 8방향 계산
|
|
||||||
local angle = math.atan2(dy, dx)
|
local angle = math.atan2(dy, dx)
|
||||||
local dir
|
local dir
|
||||||
if angle >= -0.393 and angle < 0.393 then dir = defines.direction.east
|
if angle >= -0.393 and angle < 0.393 then dir = defines.direction.east
|
||||||
@@ -101,7 +175,6 @@ rcon.print("WALK:" .. string.format("%.1f", dist))
|
|||||||
if result.startswith("ARRIVED"):
|
if result.startswith("ARRIVED"):
|
||||||
return True, f"({x}, {y})로 도착"
|
return True, f"({x}, {y})로 도착"
|
||||||
|
|
||||||
# 진행 안 되면 stuck 판정
|
|
||||||
try:
|
try:
|
||||||
dist = float(result.split(":")[1])
|
dist = float(result.split(":")[1])
|
||||||
if abs(dist - last_dist) < 0.3:
|
if abs(dist - last_dist) < 0.3:
|
||||||
@@ -121,10 +194,8 @@ rcon.print("WALK:" .. string.format("%.1f", dist))
|
|||||||
self.rcon.lua(P + "p.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) ────────────────────────────────
|
# ── 자원 채굴 ────────────────────────────────────────────────────
|
||||||
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(P + f"""
|
find_result = self.rcon.lua(P + f"""
|
||||||
local surface = p.surface
|
local surface = p.surface
|
||||||
local resources = surface.find_entities_filtered{{
|
local resources = surface.find_entities_filtered{{
|
||||||
@@ -144,7 +215,6 @@ end
|
|||||||
""")
|
""")
|
||||||
if not find_result or find_result in ("NO_PLAYER", "NO_CHARACTER"):
|
if not find_result or find_result in ("NO_PLAYER", "NO_CHARACTER"):
|
||||||
return False, find_result or "플레이어 없음"
|
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 먼저"
|
||||||
|
|
||||||
@@ -156,11 +226,8 @@ end
|
|||||||
max_ticks = count * 40
|
max_ticks = count * 40
|
||||||
|
|
||||||
for _ in range(max_ticks):
|
for _ in range(max_ticks):
|
||||||
self.rcon.lua(P + f"""
|
self.rcon.lua(P + f"p.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(P + "p.mining_state = {mining = false}")
|
self.rcon.lua(P + "p.mining_state = {mining = false}")
|
||||||
@@ -170,210 +237,107 @@ p.mining_state = {{mining = true, position = {{{rx}, {ry}}}}}
|
|||||||
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) ─────────────────────────────
|
# ── 제작 ─────────────────────────────────────────────────────────
|
||||||
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(P + f"""
|
result = self.rcon.lua(P + f"""
|
||||||
local recipe = p.force.recipes["{item}"]
|
local recipe = p.force.recipes["{item}"]
|
||||||
if not recipe then
|
if not recipe then rcon.print("NO_RECIPE") return end
|
||||||
rcon.print("NO_RECIPE")
|
if not recipe.enabled then rcon.print("LOCKED") return end
|
||||||
return
|
|
||||||
end
|
|
||||||
if not recipe.enabled then
|
|
||||||
rcon.print("LOCKED")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local crafted = p.begin_crafting{{count={count}, recipe="{item}"}}
|
local crafted = p.begin_crafting{{count={count}, recipe="{item}"}}
|
||||||
if crafted > 0 then
|
if crafted > 0 then rcon.print("CRAFTING:" .. crafted)
|
||||||
rcon.print("CRAFTING:" .. crafted)
|
else rcon.print("NO_INGREDIENTS") end
|
||||||
else
|
|
||||||
rcon.print("NO_INGREDIENTS")
|
|
||||||
end
|
|
||||||
""")
|
""")
|
||||||
if not result or result in ("NO_PLAYER", "NO_CHARACTER"):
|
if not result or result in ("NO_PLAYER", "NO_CHARACTER"):
|
||||||
return False, result or "플레이어 없음"
|
return False, result or "플레이어 없음"
|
||||||
|
|
||||||
if result.startswith("CRAFTING:"):
|
if result.startswith("CRAFTING:"):
|
||||||
crafted = int(result.split(":")[1])
|
crafted = int(result.split(":")[1])
|
||||||
wait_time = min(crafted * 2, 30)
|
time.sleep(min(crafted * 2, 30))
|
||||||
time.sleep(wait_time)
|
|
||||||
return True, f"{item} {crafted}개 제작 완료"
|
return True, f"{item} {crafted}개 제작 완료"
|
||||||
elif result == "NO_RECIPE":
|
elif result == "NO_RECIPE":
|
||||||
return False, f"{item} 레시피 없음 (아이템 이름 확인)"
|
return False, f"{item} 레시피 없음"
|
||||||
elif result == "LOCKED":
|
elif result == "LOCKED":
|
||||||
return False, f"{item} 레시피 잠김 (연구 필요)"
|
return False, f"{item} 레시피 잠김"
|
||||||
elif result == "NO_INGREDIENTS":
|
elif result == "NO_INGREDIENTS":
|
||||||
return False, f"{item} 재료 부족 — 필요 재료를 먼저 채굴/제작"
|
return False, f"{item} 재료 부족"
|
||||||
return False, f"제작 실패: {result}"
|
return False, f"제작 실패: {result}"
|
||||||
|
|
||||||
# ── 건물 배치 (커서 + build_from_cursor) ──────────────────────────
|
# ── 건물 배치 ────────────────────────────────────────────────────
|
||||||
def place_entity(
|
def place_entity(self, name: str, x: int, y: int, direction: str = "north") -> tuple[bool, str]:
|
||||||
self,
|
dir_map = {"north": "defines.direction.north", "south": "defines.direction.south",
|
||||||
name: str,
|
"east": "defines.direction.east", "west": "defines.direction.west"}
|
||||||
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")
|
lua_dir = dir_map.get(direction, "defines.direction.north")
|
||||||
|
|
||||||
result = self.rcon.lua(P + f"""
|
result = self.rcon.lua(P + f"""
|
||||||
local inv = p.get_main_inventory()
|
local inv = p.get_main_inventory()
|
||||||
|
|
||||||
-- 인벤토리 확인
|
|
||||||
local have = inv.get_item_count("{name}")
|
local have = inv.get_item_count("{name}")
|
||||||
if have < 1 then
|
if have < 1 then rcon.print("NO_ITEM:" .. have) return end
|
||||||
rcon.print("NO_ITEM:" .. have)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 건설 거리 확인
|
|
||||||
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)) return end
|
||||||
rcon.print("TOO_FAR:" .. string.format("%.1f", dist))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 이미 있는지 확인
|
|
||||||
local existing = p.surface.find_entity("{name}", {{{x}, {y}}})
|
local existing = p.surface.find_entity("{name}", {{{x}, {y}}})
|
||||||
if existing then
|
if existing then rcon.print("ALREADY_EXISTS") return end
|
||||||
rcon.print("ALREADY_EXISTS")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 커서에 아이템 놓고 배치
|
|
||||||
p.cursor_stack.set_stack({{name="{name}", count=1}})
|
p.cursor_stack.set_stack({{name="{name}", count=1}})
|
||||||
local built = p.build_from_cursor{{
|
local built = p.build_from_cursor{{position = {{{x}, {y}}}, direction = {lua_dir}}}
|
||||||
position = {{{x}, {y}}},
|
if built then p.cursor_stack.clear() rcon.print("OK")
|
||||||
direction = {lua_dir}
|
else p.cursor_stack.clear() rcon.print("BLOCKED") end
|
||||||
}}
|
|
||||||
|
|
||||||
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"):
|
if not result or result in ("NO_PLAYER", "NO_CHARACTER"):
|
||||||
return False, result or "플레이어 없음"
|
return False, result or "플레이어 없음"
|
||||||
|
if result == "OK": return True, f"{name} 배치 완료 ({x},{y})"
|
||||||
if result == "OK":
|
elif result.startswith("NO_ITEM"): return False, f"인벤토리에 {name} 없음"
|
||||||
return True, f"{name} 배치 완료 ({x},{y})"
|
elif result.startswith("TOO_FAR"): return False, f"({x},{y})가 너무 멀음 — move 먼저"
|
||||||
elif result.startswith("NO_ITEM"):
|
elif result == "ALREADY_EXISTS": return True, f"{name}이 이미 ({x},{y})에 있음"
|
||||||
return False, f"인벤토리에 {name} 없음 — craft_item 먼저"
|
elif result == "BLOCKED": return False, f"({x},{y}) 배치 불가"
|
||||||
elif result.startswith("TOO_FAR"):
|
|
||||||
dist = result.split(":")[1]
|
|
||||||
return False, f"({x},{y})가 너무 멀음 (거리:{dist}) — move로 가까이 이동 먼저"
|
|
||||||
elif result == "ALREADY_EXISTS":
|
|
||||||
return True, f"{name}이 이미 ({x},{y})에 있음"
|
|
||||||
elif result == "BLOCKED":
|
|
||||||
return False, f"({x},{y}) 배치 불가 — 지형 또는 건물 겹침"
|
|
||||||
return False, f"배치 실패: {result}"
|
return False, f"배치 실패: {result}"
|
||||||
|
|
||||||
# ── 벨트 라인 (걸어다니면서 하나씩 배치) ─────────────────────────
|
# ── 벨트 라인 ────────────────────────────────────────────────────
|
||||||
def place_belt_line(
|
def place_belt_line(self, from_x: int, from_y: int, to_x: int, to_y: int) -> tuple[bool, str]:
|
||||||
self,
|
placed, failed = 0, 0
|
||||||
from_x: int, from_y: int,
|
|
||||||
to_x: int, to_y: int
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
"""벨트를 한 칸씩 걸어가면서 배치."""
|
|
||||||
placed = 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
|
||||||
direction = "east" if step == 1 else "west"
|
d = "east" if step == 1 else "west"
|
||||||
for bx in range(from_x, to_x + step, step):
|
for bx in range(from_x, to_x + step, step): positions.append((bx, from_y, d))
|
||||||
positions.append((bx, from_y, direction))
|
|
||||||
|
|
||||||
if from_y != to_y:
|
if from_y != to_y:
|
||||||
step = 1 if from_y < to_y else -1
|
step = 1 if from_y < to_y else -1
|
||||||
direction = "south" if step == 1 else "north"
|
d = "south" if step == 1 else "north"
|
||||||
start_y = from_y + (step if from_x != to_x else 0)
|
sy = from_y + (step if from_x != to_x else 0)
|
||||||
for by in range(start_y, to_y + step, step):
|
for by in range(sy, to_y + step, step): positions.append((to_x, by, d))
|
||||||
positions.append((to_x, by, direction))
|
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, f"벨트 설치 실패 ({failed}개 실패)"
|
||||||
|
return True, f"벨트 {placed}개 설치 완료" + (f", {failed}개 실패" if failed else "")
|
||||||
|
|
||||||
for bx, by, direction in positions:
|
# ── 아이템 삽입 ──────────────────────────────────────────────────
|
||||||
ok, msg = self.move(bx, by)
|
def insert_to_entity(self, x: int, y: int, item: str = "coal", count: int = 50) -> tuple[bool, str]:
|
||||||
if not ok:
|
|
||||||
failed += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
ok, msg = self.place_entity("transport-belt", bx, by, direction)
|
|
||||||
if ok:
|
|
||||||
placed += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
if placed == 0:
|
|
||||||
return False, f"벨트 설치 실패 ({failed}개 실패)"
|
|
||||||
if failed > 0:
|
|
||||||
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(P + f"""
|
result = self.rcon.lua(P + f"""
|
||||||
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") return end
|
||||||
rcon.print("TOO_FAR:" .. string.format("%.1f", dist))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local inv = p.get_main_inventory()
|
local inv = p.get_main_inventory()
|
||||||
local have = inv.get_item_count("{item}")
|
local have = inv.get_item_count("{item}")
|
||||||
if have < 1 then
|
if have < 1 then rcon.print("NO_ITEM") return end
|
||||||
rcon.print("NO_ITEM:" .. have)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local actual = math.min(have, {count})
|
local actual = math.min(have, {count})
|
||||||
local entities = p.surface.find_entities_filtered{{
|
local entities = p.surface.find_entities_filtered{{position = {{{x},{y}}}, radius = 2}}
|
||||||
area = {{{{{x}-1, {y}-1}}, {{{x}+1, {y}+1}}}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
local inserted = false
|
local inserted = false
|
||||||
for _, e in ipairs(entities) do
|
for _, e in ipairs(entities) do
|
||||||
if e.valid then
|
if e.valid then
|
||||||
-- 연료 인벤토리 (채굴기, 제련소, 보일러)
|
|
||||||
if e.burner then
|
if e.burner then
|
||||||
local fi = e.burner.inventory
|
local fi = e.burner.inventory
|
||||||
if fi then
|
if fi then
|
||||||
local removed = inv.remove({{name="{item}", count=actual}})
|
local removed = inv.remove({{name="{item}", count=actual}})
|
||||||
if removed > 0 then
|
if removed > 0 then fi.insert({{name="{item}", count=removed}}) inserted = true break end
|
||||||
fi.insert({{name="{item}", count=removed}})
|
|
||||||
inserted = true
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
-- 일반 인벤토리 (상자, 연구소 등)
|
|
||||||
if not inserted then
|
if not inserted then
|
||||||
for i = 1, 10 do
|
for i = 1, 10 do
|
||||||
local target_inv = e.get_inventory(i)
|
local ti = e.get_inventory(i)
|
||||||
if target_inv and target_inv.can_insert({{name="{item}", count=1}}) then
|
if ti and ti.can_insert({{name="{item}", count=1}}) then
|
||||||
local removed = inv.remove({{name="{item}", count=actual}})
|
local removed = inv.remove({{name="{item}", count=actual}})
|
||||||
if removed > 0 then
|
if removed > 0 then ti.insert({{name="{item}", count=removed}}) inserted = true end
|
||||||
target_inv.insert({{name="{item}", count=removed}})
|
|
||||||
inserted = true
|
|
||||||
end
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -381,74 +345,40 @@ for _, e in ipairs(entities) do
|
|||||||
if inserted then break end
|
if inserted then break end
|
||||||
end
|
end
|
||||||
end
|
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"):
|
if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음"
|
||||||
return False, result or "플레이어 없음"
|
if result == "OK": return True, f"({x},{y}) 건물에 {item} 삽입 완료"
|
||||||
|
elif result == "TOO_FAR": return False, f"({x},{y})가 너무 멀음"
|
||||||
if result == "OK":
|
elif result == "NO_ITEM": return False, f"인벤토리에 {item} 없음"
|
||||||
return True, f"({x},{y}) 건물에 {item} 삽입 완료"
|
|
||||||
elif result.startswith("TOO_FAR"):
|
|
||||||
return False, f"({x},{y})가 너무 멀음 — move로 가까이 이동 먼저"
|
|
||||||
elif result.startswith("NO_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(P + f"""
|
result = self.rcon.lua(P + f"""
|
||||||
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") return end
|
||||||
rcon.print("TOO_FAR")
|
local e = p.surface.find_entities_filtered{{position = {{{x},{y}}}, radius = 2, type = "assembling-machine"}}[1]
|
||||||
return
|
if e then e.set_recipe("{recipe}") rcon.print("OK") else rcon.print("NOT_FOUND") end
|
||||||
end
|
|
||||||
|
|
||||||
local e = p.surface.find_entities_filtered{{
|
|
||||||
area = {{{{{x}-1, {y}-1}}, {{{x}+1, {y}+1}}}},
|
|
||||||
type = "assembling-machine"
|
|
||||||
}}[1]
|
|
||||||
|
|
||||||
if e then
|
|
||||||
e.set_recipe("{recipe}")
|
|
||||||
rcon.print("OK")
|
|
||||||
else
|
|
||||||
rcon.print("NOT_FOUND")
|
|
||||||
end
|
|
||||||
""")
|
""")
|
||||||
if not result or result in ("NO_PLAYER", "NO_CHARACTER"):
|
if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음"
|
||||||
return False, result or "플레이어 없음"
|
if result == "OK": return True, f"({x},{y}) 레시피: {recipe}"
|
||||||
|
elif result == "TOO_FAR": return False, f"({x},{y})가 너무 멀음"
|
||||||
if result == "OK":
|
|
||||||
return True, f"({x},{y}) 레시피: {recipe}"
|
|
||||||
elif result == "TOO_FAR":
|
|
||||||
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(P + f"""
|
result = self.rcon.lua(P + f"""
|
||||||
local force = p.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")
|
elseif t.researched then rcon.print("ALREADY_DONE")
|
||||||
elseif t.researched then
|
else force.research_queue_enabled = true force.add_research("{tech}") rcon.print("OK") end
|
||||||
rcon.print("ALREADY_DONE")
|
|
||||||
else
|
|
||||||
force.research_queue_enabled = true
|
|
||||||
force.add_research("{tech}")
|
|
||||||
rcon.print("OK")
|
|
||||||
end
|
|
||||||
""")
|
""")
|
||||||
if not result or result in ("NO_PLAYER", "NO_CHARACTER"):
|
if not result or result in ("NO_PLAYER", "NO_CHARACTER"): return False, result or "플레이어 없음"
|
||||||
return False, result or "플레이어 없음"
|
if result == "OK": return True, f"{tech} 연구 시작"
|
||||||
|
elif result == "ALREADY_DONE": return True, f"{tech} 이미 완료"
|
||||||
if result == "OK":
|
elif result == "NO_TECH": return False, f"{tech} 기술 없음"
|
||||||
return True, f"{tech} 연구 시작"
|
|
||||||
elif result == "ALREADY_DONE":
|
|
||||||
return True, f"{tech} 이미 완료"
|
|
||||||
elif result == "NO_TECH":
|
|
||||||
return False, f"{tech} 기술 없음"
|
|
||||||
return False, result
|
return False, result
|
||||||
|
|
||||||
# ── 대기 ─────────────────────────────────────────────────────────
|
# ── 대기 ─────────────────────────────────────────────────────────
|
||||||
@@ -458,10 +388,6 @@ end
|
|||||||
|
|
||||||
# ── 유틸리티 ─────────────────────────────────────────────────────
|
# ── 유틸리티 ─────────────────────────────────────────────────────
|
||||||
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(P + f'rcon.print(tostring(p.get_main_inventory().get_item_count("{item}")))')
|
||||||
rcon.print(tostring(p.get_main_inventory().get_item_count("{item}")))
|
try: return int(result)
|
||||||
""")
|
except: return 0
|
||||||
try:
|
|
||||||
return int(result)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return 0
|
|
||||||
|
|||||||
Reference in New Issue
Block a user