diff --git a/action_executor.py b/action_executor.py index 7f1a0fc..5c33127 100644 --- a/action_executor.py +++ b/action_executor.py @@ -28,6 +28,7 @@ class ActionExecutor: "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, + "build_smelting_line": self.build_smelting_line, } handler = handlers.get(act) 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 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]: result = self.rcon.lua(P + f""" diff --git a/tests/test_build_smelting_line.py b/tests/test_build_smelting_line.py new file mode 100644 index 0000000..fc9f31a --- /dev/null +++ b/tests/test_build_smelting_line.py @@ -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()