Files
factorio-ai-agent/state_reader.py
21in7 e98d08bb44 fix: 개선된 인벤토리 판독 로직으로 안정성 향상
- 인벤토리 판독 시 `inv.get_contents()`를 우선 사용하여 일부 환경에서 발생할 수 있는 오류를 줄임
- 이전 방식인 인덱스 접근 방식은 호환성 문제를 해결하기 위한 폴백으로 유지
- README.md에 변경 사항 반영
2026-03-25 21:56:26 +09:00

197 lines
6.5 KiB
Python

"""
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)