feat: add build_smelting_line blueprint action to ActionExecutor
This commit is contained in:
@@ -28,6 +28,7 @@ class ActionExecutor:
|
|||||||
"place_entity": self.place_entity, "place_belt_line": self.place_belt_line,
|
"place_entity": self.place_entity, "place_belt_line": self.place_belt_line,
|
||||||
"insert_to_entity": self.insert_to_entity, "set_recipe": self.set_recipe,
|
"insert_to_entity": self.insert_to_entity, "set_recipe": self.set_recipe,
|
||||||
"start_research": self.start_research, "wait": self.wait,
|
"start_research": self.start_research, "wait": self.wait,
|
||||||
|
"build_smelting_line": self.build_smelting_line,
|
||||||
}
|
}
|
||||||
handler = handlers.get(act)
|
handler = handlers.get(act)
|
||||||
if not handler:
|
if not handler:
|
||||||
@@ -454,6 +455,54 @@ p.mining_state = {{mining = true, position = {{x = {mine_x}, y = {mine_y}}}}}
|
|||||||
return True, f"{ore} {total_mined}개 채굴 (목표 {count}개 중 일부)"
|
return True, f"{ore} {total_mined}개 채굴 (목표 {count}개 중 일부)"
|
||||||
return False, f"{ore} 채굴 실패 - {len(failed_positions)}개 타일 접근 불가"
|
return False, f"{ore} 채굴 실패 - {len(failed_positions)}개 타일 접근 불가"
|
||||||
|
|
||||||
|
# ── 건설 자동화 (Blueprint) ───────────────────────────────────────
|
||||||
|
def build_smelting_line(
|
||||||
|
self,
|
||||||
|
ore: str = "iron-ore",
|
||||||
|
x: int = 0,
|
||||||
|
y: int = 0,
|
||||||
|
furnace_count: int = 4,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
석탄 제련 라인 자동 배치.
|
||||||
|
stone-furnace를 y축 방향으로 furnace_count개 일렬 배치하고
|
||||||
|
각각에 석탄 + 광석을 자동 투입한다.
|
||||||
|
"""
|
||||||
|
spacing = 3 # furnace 간격 (타일)
|
||||||
|
placed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for i in range(furnace_count):
|
||||||
|
fy = y + i * spacing
|
||||||
|
ok, msg = self.move(x, fy)
|
||||||
|
if not ok:
|
||||||
|
print(f" [blueprint] ({x},{fy}) 이동 실패: {msg}")
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
ok, msg = self.place_entity(name="stone-furnace", x=x, y=fy)
|
||||||
|
if not ok:
|
||||||
|
print(f" [blueprint] ({x},{fy}) 배치 실패: {msg}")
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 실제 배치된 좌표 파싱 (REUSED 포함)
|
||||||
|
parsed = self._extract_coords_from_message(msg)
|
||||||
|
rx, ry = (parsed if parsed is not None else (x, fy))
|
||||||
|
|
||||||
|
boot_ok, boot_msg = self._auto_bootstrap_furnace(
|
||||||
|
furnace_name="stone-furnace",
|
||||||
|
x=int(rx),
|
||||||
|
y=int(ry),
|
||||||
|
reason=f"build_smelting_line: {ore}",
|
||||||
|
)
|
||||||
|
placed += 1
|
||||||
|
print(f" [blueprint] ({rx},{ry}) OK — bootstrap: {boot_msg}")
|
||||||
|
|
||||||
|
if placed == 0:
|
||||||
|
return False, f"build_smelting_line 실패: {failed}개 모두 배치 불가"
|
||||||
|
return True, f"build_smelting_line: {placed}개 배치 완료 ({failed}개 실패)"
|
||||||
|
|
||||||
# ── 제작/배치/삽입/레시피/연구/대기 ──────────────────────────────
|
# ── 제작/배치/삽입/레시피/연구/대기 ──────────────────────────────
|
||||||
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"""
|
||||||
|
|||||||
63
tests/test_build_smelting_line.py
Normal file
63
tests/test_build_smelting_line.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from action_executor import ActionExecutor
|
||||||
|
|
||||||
|
|
||||||
|
def make_executor():
|
||||||
|
rcon = MagicMock()
|
||||||
|
rcon.lua.return_value = "OK"
|
||||||
|
ex = ActionExecutor(rcon)
|
||||||
|
return ex, rcon
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildSmeltingLine(unittest.TestCase):
|
||||||
|
def test_build_smelting_line_calls_place_entity_n_times(self):
|
||||||
|
ex, rcon = make_executor()
|
||||||
|
with patch.object(ex, "place_entity", return_value=(True, "stone-furnace 배치 (0, 0)")) as mock_place, \
|
||||||
|
patch.object(ex, "_auto_bootstrap_furnace", return_value=(True, "OK")), \
|
||||||
|
patch.object(ex, "move", return_value=(True, "도착")):
|
||||||
|
ok, msg = ex.build_smelting_line(ore="iron-ore", x=0, y=0, furnace_count=3)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertEqual(mock_place.call_count, 3)
|
||||||
|
|
||||||
|
def test_build_smelting_line_default_furnace_count(self):
|
||||||
|
ex, rcon = make_executor()
|
||||||
|
with patch.object(ex, "place_entity", return_value=(True, "stone-furnace 배치 (0, 0)")), \
|
||||||
|
patch.object(ex, "_auto_bootstrap_furnace", return_value=(True, "OK")), \
|
||||||
|
patch.object(ex, "move", return_value=(True, "도착")):
|
||||||
|
ok, msg = ex.build_smelting_line(ore="iron-ore", x=0, y=0)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
def test_build_smelting_line_partial_failure_still_reports(self):
|
||||||
|
ex, rcon = make_executor()
|
||||||
|
# 첫 번째 성공, 두 번째 실패, 세 번째 성공
|
||||||
|
side_effects = [(True, "stone-furnace 배치 (0, 0)"), (False, "막힘"), (True, "stone-furnace 배치 (0, 6)")]
|
||||||
|
with patch.object(ex, "place_entity", side_effect=side_effects), \
|
||||||
|
patch.object(ex, "_auto_bootstrap_furnace", return_value=(True, "OK")), \
|
||||||
|
patch.object(ex, "move", return_value=(True, "도착")):
|
||||||
|
ok, msg = ex.build_smelting_line(ore="iron-ore", x=0, y=0, furnace_count=3)
|
||||||
|
# 2개 성공이므로 전체 실패가 아님
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertIn("2개 배치", msg)
|
||||||
|
|
||||||
|
def test_build_smelting_line_all_fail_returns_false(self):
|
||||||
|
ex, rcon = make_executor()
|
||||||
|
with patch.object(ex, "place_entity", return_value=(False, "막힘")), \
|
||||||
|
patch.object(ex, "_auto_bootstrap_furnace", return_value=(True, "OK")), \
|
||||||
|
patch.object(ex, "move", return_value=(True, "도착")):
|
||||||
|
ok, msg = ex.build_smelting_line(ore="iron-ore", x=0, y=0, furnace_count=2)
|
||||||
|
self.assertFalse(ok)
|
||||||
|
|
||||||
|
def test_build_smelting_line_via_execute_dispatch(self):
|
||||||
|
ex, rcon = make_executor()
|
||||||
|
with patch.object(ex, "build_smelting_line", return_value=(True, "2개 배치 완료")) as mock_bsl:
|
||||||
|
ok, msg = ex.execute({
|
||||||
|
"action": "build_smelting_line",
|
||||||
|
"params": {"ore": "iron-ore", "x": -90, "y": -70, "furnace_count": 4},
|
||||||
|
})
|
||||||
|
mock_bsl.assert_called_once_with(ore="iron-ore", x=-90, y=-70, furnace_count=4)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user