""" state_reader.py RCON을 통해 팩토리오 게임 상태를 읽어오는 모듈 핵심 변경: - f-string 제거: Lua 중괄호와 Python f-string 충돌 방지 - position+radius 방식 사용 (area 중첩 중괄호 문제 해결) - 모든 Lua 코드 pcall로 감싸기 - 모든 Python 파싱 try/except 감싸기 """ import json from factorio_rcon import FactorioRCON P = 'local p = game.players[1] if not p then rcon.print("{}") return end ' class StateReader: def __init__(self, rcon: FactorioRCON): self.rcon = rcon def get_full_state(self) -> dict: return { "player": self.get_player_info(), "inventory": self.get_inventory(), "resources": self.scan_resources(), "buildings": self.get_buildings(), "tech": self.get_research_status(), } def get_player_info(self) -> dict: lua = P + 'rcon.print(game.table_to_json({x=math.floor(p.position.x), y=math.floor(p.position.y), health=p.character and p.character.health or 100}))' try: raw = self.rcon.lua(lua) return json.loads(raw) if raw and raw.startswith("{") else {} except Exception: return {} def get_inventory(self) -> dict: # Factorio 2.0 get_contents() 호환: pcall로 안전하게 lua = P + """ local ok, err = pcall(function() local inv = p.get_main_inventory() if not inv then rcon.print("{}") return end local result = {} if inv.get_contents then -- Factorio 2.0: get_contents()가 있으면 가장 안정적으로 읽을 수 있음 result = inv.get_contents() else -- 호환용 폴백 (일부 버전에서 #inv / 인덱스 접근이 불안정할 수 있음) for i = 1, #inv do local stack = inv[i] if stack.valid_for_read then result[stack.name] = (result[stack.name] or 0) + stack.count end end end rcon.print(game.table_to_json(result)) end) if not ok then rcon.print("{}") end """ try: raw = self.rcon.lua(lua) return json.loads(raw) if raw and raw.startswith("{") else {} except Exception: return {} def scan_resources(self) -> dict: """position+radius 방식으로 자원 스캔 (f-string 중괄호 문제 완전 회피)""" lua = P + """ local ok, err = pcall(function() local surface = p.surface local all_res = surface.find_entities_filtered{position = p.position, radius = 500, type = "resource"} if not all_res or #all_res == 0 then rcon.print("{}") return end local resources = {} for _, e in ipairs(all_res) do local name = e.name if not resources[name] then resources[name] = {count = 0, sx = 0, sy = 0} end local r = resources[name] r.count = r.count + 1 r.sx = r.sx + e.position.x r.sy = r.sy + e.position.y end local out = {} for name, r in pairs(resources) do out[name] = { count = r.count, center_x = math.floor(r.sx / r.count), center_y = math.floor(r.sy / r.count) } end rcon.print(game.table_to_json(out)) end) if not ok then rcon.print("{}") end """ try: raw = self.rcon.lua(lua) return json.loads(raw) if raw and raw.startswith("{") else {} except Exception: return {} def get_buildings(self) -> dict: """type 기반 검색 (f-string 없음, pcall 안전)""" lua = P + """ local ok, err = pcall(function() local surface = p.surface local result = {} local all = surface.find_entities_filtered{position = p.position, radius = 300, force = "player"} for _, e in ipairs(all) do if e.name ~= "character" then result[e.name] = (result[e.name] or 0) + 1 end end rcon.print(game.table_to_json(result)) end) if not ok then rcon.print("{}") end """ try: raw = self.rcon.lua(lua) return json.loads(raw) if raw and raw.startswith("{") else {} except Exception: return {} def get_research_status(self) -> dict: lua = P + """ local ok, err = pcall(function() local force = p.force local k = 0 for name, tech in pairs(force.technologies) do if tech.researched then k = k + 1 end end rcon.print(game.table_to_json({ current = force.current_research and force.current_research.name or "none", completed_count = k })) end) if not ok then rcon.print("{}") end """ try: raw = self.rcon.lua(lua) return json.loads(raw) if raw and raw.startswith("{") else {} except Exception: return {} def summarize_for_ai(self, state: dict) -> str: p = state.get("player", {}) inv = state.get("inventory", {}) res = state.get("resources", {}) bld = state.get("buildings", {}) lines = [ "## 현재 게임 상태", "### 플레이어", f"- 위치: ({p.get('x', '?')}, {p.get('y', '?')})", f"- 체력: {p.get('health', '?')}", "", "### 인벤토리", ] if inv: for item, count in sorted(inv.items(), key=lambda x: -x[1])[:15]: lines.append(f"- {item}: {count}개") else: lines.append("- 비어 있음") lines += ["", "### 주변 자원 패치 (반경 500타일 스캔)"] if res: px = p.get('x', 0) py = p.get('y', 0) sorted_res = sorted( res.items(), key=lambda item: ((item[1].get('center_x',0) - px)**2 + (item[1].get('center_y',0) - py)**2) ) for ore, info in sorted_res: dist = int(((info.get('center_x',0) - px)**2 + (info.get('center_y',0) - py)**2)**0.5) lines.append( f"- {ore}: {info.get('count',0)}타일 " f"(중심: {info.get('center_x','?')}, {info.get('center_y','?')}) " f"[거리: ~{dist}타일]" ) else: lines.append("- 반경 500타일 내 자원 없음 — 더 멀리 탐색 필요") lines += ["", "### 건설된 건물"] if bld: for name, count in sorted(bld.items(), key=lambda x: -x[1])[:10]: lines.append(f"- {name}: {count}개") else: lines.append("- 아직 건물 없음") return "\n".join(lines)