feat: 자동 부트스트랩 기능 추가 및 인벤토리 검사 개선

- `stone-furnace` 배치 후 자동으로 `coal`과 `iron-ore`/`copper-ore`를 투입하여 제련이 시작되도록 보정하는 기능 추가
- `insert_to_entity` 호출 전에 `can_insert` 여부를 확인하여 아이템 증발 위험을 줄이는 로직 개선
- `README.md`에 새로운 기능 설명 추가
- `tests/test_furnace_bootstrap.py` 및 `tests/test_insert_to_entity.py`에 대한 단위 테스트 추가
This commit is contained in:
21in7
2026-03-26 00:00:54 +09:00
parent 25eaa7f6cd
commit 9b3d26aa12
7 changed files with 227 additions and 4 deletions

View File

@@ -37,10 +37,102 @@ class ActionExecutor:
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
success, message = result
# stone-furnace 배치 직후 자동 부트스트랩(연료+광석 투입)
# - AI가 insert_to_entity를 누락해도 제련이 시작되게 보정
if act == "place_entity" and success:
furnace_name = params.get("name")
if furnace_name == "stone-furnace":
rx, ry = params.get("x"), params.get("y")
parsed = self._extract_coords_from_message(message)
if parsed is not None:
rx, ry = parsed
boot_ok, boot_msg = self._auto_bootstrap_furnace(
furnace_name=furnace_name,
x=int(rx),
y=int(ry),
reason=action.get("reason", "") or "",
)
message = f"{message} / bootstrap: {'OK' if boot_ok else 'FAIL'} {boot_msg}"
return success, message
except TypeError as e: return False, f"파라미터 오류: {e}"
except Exception as e: return False, f"실행 오류: {e}"
def _extract_coords_from_message(self, message: str) -> tuple[int, int] | None:
"""
place_entity 성공 메시지에서 실제로 지어진 좌표를 파싱.
예) "stone-furnace 배치 (-91, -68)" / "stone-furnace 배치 (-91,-68)"
"""
import re
if not isinstance(message, str):
return None
m = re.search(r"\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)", message)
if not m:
return None
try:
x = int(round(float(m.group(1))))
y = int(round(float(m.group(2))))
return x, y
except Exception:
return None
def _auto_bootstrap_furnace(
self,
*,
furnace_name: str,
x: int,
y: int,
reason: str = "",
) -> tuple[bool, str]:
"""
stone-furnace 배치 직후:
- coal(연료) 투입
- iron-ore 또는 copper-ore 투입(가능한 것 우선)
반환값은 현재 전략에서 성공/실패를 상위에 강제하지 않지만,
디버깅을 위해 메시지를 남긴다.
"""
if furnace_name != "stone-furnace":
return False, "unsupported_furnace"
coal_have = self._get_item_count("coal")
if coal_have <= 0:
return False, "no_coal"
ore_have_iron = self._get_item_count("iron-ore")
ore_have_copper = self._get_item_count("copper-ore")
# AI가 reason에 어떤 ore를 의도했는지 우선 반영(있을 때만).
preferred_ore: str | None = None
if "iron-ore" in reason:
preferred_ore = "iron-ore"
elif "copper-ore" in reason:
preferred_ore = "copper-ore"
if preferred_ore == "iron-ore" and ore_have_iron > 0:
ore = "iron-ore"
elif preferred_ore == "copper-ore" and ore_have_copper > 0:
ore = "copper-ore"
elif ore_have_iron > 0:
ore = "iron-ore"
elif ore_have_copper > 0:
ore = "copper-ore"
else:
return False, "no_ore"
# insert_to_entity는 내부적으로 inv.remove를 수행하므로,
# count는 "최대 50, 현재 보유량"으로 제한해 과잉/오차를 줄인다.
coal_count = min(50, coal_have)
ore_count = min(50, self._get_item_count(ore))
ok1, msg1 = self.insert_to_entity(x, y, item="coal", count=coal_count)
ok2, msg2 = self.insert_to_entity(x, y, item=ore, count=ore_count)
return ok1 and ok2, f"bootstrap coal+ore: {msg1}; {msg2}"
# ── 탐색 ─────────────────────────────────────────────────────────
def explore(
self,
@@ -459,9 +551,13 @@ local inserted = false
for _, e in ipairs(entities) do
if e.valid and e.burner then
local fi = e.burner.inventory
if fi then
if fi and fi.can_insert({{name="{item}", count=1}}) then
-- can_insert 이후에만 inv에서 제거해서, 미삽입/미스매치로 인한 아이템 증발을 방지
local removed = inv.remove({{name="{item}", count=actual}})
if removed > 0 then fi.insert({{name="{item}", count=removed}}) inserted = true break end
if removed > 0 then
local inserted_count = fi.insert({{name="{item}", count=removed}})
if inserted_count and inserted_count > 0 then inserted = true break end
end
end
end
if not inserted and e.valid then
@@ -469,7 +565,10 @@ for _, e in ipairs(entities) do
local ti = e.get_inventory(i)
if ti and ti.can_insert({{name="{item}", count=1}}) then
local removed = inv.remove({{name="{item}", count=actual}})
if removed > 0 then ti.insert({{name="{item}", count=removed}}) inserted = true end
if removed > 0 then
local inserted_count = ti.insert({{name="{item}", count=removed}})
if inserted_count and inserted_count > 0 then inserted = true end
end
break
end
end