fix: f-string 완전 제거 + position/radius 방식 + pcall + try/except
근본 원인: 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("{") 검증
This commit is contained in:
241
state_reader.py
241
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("- 아직 건물 없음")
|
||||
|
||||
Reference in New Issue
Block a user