From ba013bf461ed025676fd6f78f6c4fad16379fa38 Mon Sep 17 00:00:00 2001 From: gihyeon Date: Wed, 25 Mar 2026 20:20:47 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20f-string=20=EC=99=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20+=20position/radius=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=20+=20pcall=20+=20try/except?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 근본 원인: f-string 안의 Lua {{}} 중괄호가 Python 이스케이프와 충돌 - scan_resources: area 대신 position+radius (중첩 중괄호 없음) - 모든 Lua: f-string 제거, pcall 감싸기 - get_inventory: get_contents() 대신 인덱스 접근 (Factorio 2.0 호환) - get_buildings: force="player"로 간결하게 - 모든 Python 파싱: try/except + startswith("{") 검증 --- state_reader.py | 241 ++++++++++++++++++++++-------------------------- 1 file changed, 110 insertions(+), 131 deletions(-) diff --git a/state_reader.py b/state_reader.py index 4baef6f..9b59f77 100644 --- a/state_reader.py +++ b/state_reader.py @@ -3,17 +3,16 @@ state_reader.py RCON을 통해 팩토리오 게임 상태를 읽어오는 모듈 핵심 변경: -- game.player → game.players[1] (RCON 호환) -- scan_resources 반경 200→500 (초반 탐색 개선) -- get_buildings: name 대신 type 기반 검색 (Factorio 2.0 호환) +- 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 -""" +P = 'local p = game.players[1] if not p then rcon.print("{}") return end ' class StateReader: @@ -30,127 +29,117 @@ class StateReader: } 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 -})) -""" - raw = self.rcon.lua(lua) - return json.loads(raw) if raw else {} + 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 inv = p.get_main_inventory() -if not inv then rcon.print("{}") return end -local result = {} -local contents = inv.get_contents() -for name, count in pairs(contents) do - result[name] = count -end -rcon.print(game.table_to_json(result)) -""" - raw = self.rcon.lua(lua) - return json.loads(raw) if raw else {} - - def scan_resources(self, radius: int = 500) -> dict: - """플레이어 주변 자원 패치 위치 탐색 (반경 500타일)""" - lua = P + f""" -local surface = p.surface -local center = p.position -local resources = {{}} - --- type="resource"로 모든 자원을 한번에 검색 (이름 호환 문제 없음) -local all_res = surface.find_entities_filtered{{ - area = {{ - {{center.x - {radius}, center.y - {radius}}}, - {{center.x + {radius}, center.y + {radius}}} - }}, - type = "resource" -}} - --- 자원 이름별로 그룹핑 -for _, e in ipairs(all_res) do - local name = e.name - if not resources[name] then - resources[name] = {{count = 0, sx = 0, sy = 0}} +local ok, err = pcall(function() + local inv = p.get_main_inventory() + if not inv then rcon.print("{}") return end + local result = {} + 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 - 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 result = {{}} -for name, r in pairs(resources) do - result[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(result)) + rcon.print(game.table_to_json(result)) +end) +if not ok then rcon.print("{}") end """ - raw = self.rcon.lua(lua) - return json.loads(raw) if raw else {} + try: + raw = self.rcon.lua(lua) + return json.loads(raw) if raw and raw.startswith("{") else {} + except Exception: + return {} - def get_buildings(self, radius: int = 300) -> dict: - """type 기반 검색으로 Factorio 2.0 호환""" - lua = P + f""" -local surface = p.surface -local center = p.position -local area = {{ - {{center.x - {radius}, center.y - {radius}}}, - {{center.x + {radius}, center.y + {radius}}} -}} + 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 {} --- type 기반으로 검색 (엔티티 이름 변경에 안전) -local types = {{ - "mining-drill", "furnace", "assembling-machine", - "transport-belt", "inserter", "electric-pole", - "generator", "boiler", "offshore-pump", - "lab", "radar", "container", - "ammo-turret", "electric-turret", "wall", "gate", - "oil-refinery", "chemical-plant", "mining-drill" -}} - -local result = {{}} -for _, t in ipairs(types) do - local ok, entities = pcall(function() - return surface.find_entities_filtered{{area = area, type = t}} - end) - if ok and entities and #entities > 0 then - -- 이름별로 세분화 - for _, e in ipairs(entities) do + 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 -end -rcon.print(game.table_to_json(result)) + rcon.print(game.table_to_json(result)) +end) +if not ok then rcon.print("{}") end """ - raw = self.rcon.lua(lua) - return json.loads(raw) if raw else {} + 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 force = p.force -local completed = {} -local k = 0 -for name, tech in pairs(force.technologies) do - if tech.researched then - k = k + 1 - completed[k] = name - end -end -rcon.print(game.table_to_json({ - current = force.current_research and force.current_research.name or "none", - completed_count = k -})) +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 """ - raw = self.rcon.lua(lua) - return json.loads(raw) if raw else {} + 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", {}) @@ -164,38 +153,28 @@ rcon.print(game.table_to_json({ f"- 위치: ({p.get('x', '?')}, {p.get('y', '?')})", f"- 체력: {p.get('health', '?')}", "", - "### 인벤토리 (주요 아이템)", + "### 인벤토리", ] - key_items = [ - "iron-ore", "copper-ore", "coal", "stone", - "iron-plate", "copper-plate", "steel-plate", - "burner-mining-drill", "electric-mining-drill", - "stone-furnace", "transport-belt", "inserter", - "burner-inserter", "small-electric-pole", - ] - for item in key_items: - count = inv.get(item, 0) - if count > 0: + if inv: + for item, count in sorted(inv.items(), key=lambda x: -x[1])[:15]: lines.append(f"- {item}: {count}개") - - if not any(inv.get(item, 0) > 0 for item in key_items): + 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]['center_x'] - px)**2 + (item[1]['center_y'] - py)**2) + 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['center_x'] - px)**2 + (info['center_y'] - py)**2)**0.5) + dist = int(((info.get('center_x',0) - px)**2 + (info.get('center_y',0) - py)**2)**0.5) lines.append( - f"- {ore}: {info['count']}타일 " - f"(중심: {info['center_x']}, {info['center_y']}) " + f"- {ore}: {info.get('count',0)}타일 " + f"(중심: {info.get('center_x','?')}, {info.get('center_y','?')}) " f"[거리: ~{dist}타일]" ) else: @@ -203,7 +182,7 @@ rcon.print(game.table_to_json({ lines += ["", "### 건설된 건물"] if bld: - for name, count in bld.items(): + for name, count in sorted(bld.items(), key=lambda x: -x[1])[:10]: lines.append(f"- {name}: {count}개") else: lines.append("- 아직 건물 없음")