Files
factorio-ai-agent/context_compressor.py

377 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
context_compressor.py — 계층적 컨텍스트 압축
중반부 이후 건물이 수백 개가 되면 전체 상태를 프롬프트에
담을 수 없음. 이 모듈은 공장을 '구역(zone)'으로 나눠
AI가 소화할 수 있는 크기로 압축한다.
핵심 변경: game.player → game.players[1] (RCON 호환)
"""
import json
import math
from factorio_rcon import FactorioRCON
# 공장을 이 크기의 격자로 나눔 (타일 단위)
ZONE_SIZE = 64
# 모든 Lua 코드 앞에 붙일 플레이어 참조
P = """local p = game.players[1]
if not p then rcon.print("{}") return end
"""
class ContextCompressor:
"""
게임 상태를 AI가 소화 가능한 크기로 압축.
압축 구조:
Level 0 (raw) : 모든 건물 좌표 + 상태 (토큰 수천)
Level 1 (zone) : 구역별 건물 수 + 상태 요약 (토큰 ~300)
Level 2 (global): 전체 공장 핵심 지표 + 문제 목록 (토큰 ~100)
"""
def __init__(self, rcon: FactorioRCON):
self.rcon = rcon
# ── 공개 API ────────────────────────────────────────────────────
def get_compressed_state(self, detail_level: int = 1) -> str:
"""
detail_level:
0 = 글로벌 요약만 (후반 대규모 공장, 토큰 ~80)
1 = 구역별 요약 (중반, 토큰 ~250) ← 기본값
2 = 구역 요약 + 문제 구역 드릴다운 (토큰 ~400)
"""
global_summary = self._get_global_summary()
problems = self._detect_problems()
player = self._get_player_info()
if detail_level == 0:
return self._format_global(global_summary, problems, player)
zones = self._get_zone_summaries()
if detail_level == 1:
return self._format_level1(global_summary, zones, problems, player)
# level 2: 문제 구역은 드릴다운
drilldown = self._drilldown_problem_zones(zones, problems)
return self._format_level2(global_summary, zones, problems, drilldown, player)
# ── 글로벌 지표 ─────────────────────────────────────────────────
def _get_global_summary(self) -> dict:
lua = P + """
local surface = p.surface
local force = p.force
local center = p.position
-- 전체 건물 수 집계
local counts = {}
local entity_types = {
"burner-mining-drill","electric-mining-drill",
"stone-furnace","steel-furnace","electric-furnace",
"assembling-machine-1","assembling-machine-2","assembling-machine-3",
"transport-belt","fast-transport-belt","express-transport-belt",
"inserter","fast-inserter","filter-inserter",
"small-electric-pole","medium-electric-pole","big-electric-pole",
"steam-engine","solar-panel","accumulator",
"boiler","offshore-pump","pipe",
"lab","rocket-silo","radar",
"gun-turret","laser-turret","wall","gate",
"oil-refinery","chemical-plant","pumpjack"
}
local total = 0
for _, name in ipairs(entity_types) do
local found = surface.find_entities_filtered{
area={{center.x-500,center.y-500},{center.x+500,center.y+500}},
name=name
}
if #found > 0 then
counts[name] = #found
total = total + #found
end
end
-- 연구 진행
local current_tech = "none"
local tech_progress = 0
if force.current_research then
current_tech = force.current_research.name
tech_progress = math.floor(force.research_progress * 100)
end
-- 완료된 연구 수
local researched = 0
for _, t in pairs(force.technologies) do
if t.researched then researched = researched + 1 end
end
rcon.print(game.table_to_json({
total_entities = total,
counts = counts,
current_research = current_tech,
research_progress = tech_progress,
researched_count = researched,
evolution = math.floor(force.evolution_factor * 100)
}))
"""
raw = self.rcon.lua(lua)
return json.loads(raw) if raw else {}
# ── 구역 요약 ───────────────────────────────────────────────────
def _get_zone_summaries(self) -> list[dict]:
"""공장을 ZONE_SIZE×ZONE_SIZE 격자로 나눠 구역별 요약"""
lua = P + f"""
local surface = p.surface
local center = p.position
local Z = {ZONE_SIZE}
local radius = 512
local zones = {{}}
local function zone_key(x, y)
return math.floor(x/Z) .. "," .. math.floor(y/Z)
end
local all_entities = surface.find_entities_filtered{{
area={{{{center.x-radius, center.y-radius}},
{{center.x+radius, center.y+radius}}}},
force = "player"
}}
for _, e in ipairs(all_entities) do
local key = zone_key(e.position.x, e.position.y)
if not zones[key] then
zones[key] = {{
key = key,
x = math.floor(e.position.x/Z)*Z,
y = math.floor(e.position.y/Z)*Z,
entities = 0,
miners = 0, furnaces = 0, assemblers = 0,
belts = 0, inserters = 0, poles = 0,
turrets = 0, labs = 0, pumps = 0
}}
end
local z = zones[key]
z.entities = z.entities + 1
local n = e.name
if n:find("mining%-drill") then z.miners = z.miners + 1
elseif n:find("furnace") then z.furnaces = z.furnaces + 1
elseif n:find("assembling") then z.assemblers = z.assemblers + 1
elseif n:find("belt") then z.belts = z.belts + 1
elseif n:find("inserter") then z.inserters = z.inserters + 1
elseif n:find("pole") then z.poles = z.poles + 1
elseif n:find("turret") then z.turrets = z.turrets + 1
elseif n == "lab" then z.labs = z.labs + 1
elseif n == "pumpjack" or n == "offshore-pump" then z.pumps = z.pumps + 1
end
end
local result = {{}}
for _, z in pairs(zones) do
if z.entities > 2 then
result[#result+1] = z
end
end
rcon.print(game.table_to_json(result))
"""
raw = self.rcon.lua(lua)
return json.loads(raw) if raw else []
# ── 문제 감지 ───────────────────────────────────────────────────
def _detect_problems(self) -> list[str]:
lua = P + """
local surface = p.surface
local force = p.force
local center = p.position
local problems = {}
-- 1. 연료 없는 채굴기/제련소
local burner_entities = surface.find_entities_filtered{
area={{center.x-300,center.y-300},{center.x+300,center.y+300}},
type={"mining-drill","furnace"}
}
local no_fuel = 0
for _, e in ipairs(burner_entities) do
if e.burner then
local fi = e.burner.inventory
if fi and fi.is_empty() then
no_fuel = no_fuel + 1
end
end
end
if no_fuel > 0 then
problems[#problems+1] = "연료 부족: " .. no_fuel .. "개 건물이 연료 없음"
end
-- 2. 꽉 찬 삽입기 (병목)
local inserters = surface.find_entities_filtered{
area={{center.x-300,center.y-300},{center.x+300,center.y+300}},
type="inserter"
}
local stuck = 0
for _, ins in ipairs(inserters) do
if ins.held_stack and ins.held_stack.valid_for_read then
stuck = stuck + 1
end
end
if stuck > 5 then
problems[#problems+1] = "벨트 병목: 삽입기 " .. stuck .. "개가 아이템 들고 멈춤"
end
-- 3. 전력 부족
local consumers = surface.find_entities_filtered{
area={{center.x-400,center.y-400},{center.x+400,center.y+400}},
type={"assembling-machine","lab","electric-mining-drill","electric-furnace"}
}
local no_power = 0
for _, e in ipairs(consumers) do
if e.energy < e.electric_buffer_size * 0.1 then
no_power = no_power + 1
end
end
if no_power > 0 then
problems[#problems+1] = "전력 부족: " .. no_power .. "개 건물 전력 10% 미만"
end
-- 4. 자원 고갈 임박한 굴맥
local drills = surface.find_entities_filtered{
area={{center.x-300,center.y-300},{center.x+300,center.y+300}},
type="mining-drill"
}
local depleting = 0
for _, d in ipairs(drills) do
local ore = surface.find_entities_filtered{
area={{d.position.x-3, d.position.y-3},
{d.position.x+3, d.position.y+3}},
type="resource"
}
if #ore > 0 and ore[1].amount < 5000 then
depleting = depleting + 1
end
end
if depleting > 0 then
problems[#problems+1] = "자원 고갈 임박: " .. depleting .. "개 채굴기 구역 5000 미만"
end
rcon.print(game.table_to_json(problems))
"""
raw = self.rcon.lua(lua)
try:
return json.loads(raw) if raw else []
except Exception:
return []
# ── 드릴다운 (문제 구역 상세) ────────────────────────────────────
def _drilldown_problem_zones(self, zones: list, problems: list) -> str:
if not problems or not zones:
return ""
top = sorted(zones, key=lambda z: z.get("entities", 0), reverse=True)[:2]
lines = ["#### 주요 구역 상세"]
for z in top:
lines.append(
f" 구역({z['x']},{z['y']}): "
f"채굴기{z.get('miners',0)} 제련소{z.get('furnaces',0)} "
f"조립기{z.get('assemblers',0)} 벨트{z.get('belts',0)} "
f"삽입기{z.get('inserters',0)} 전선주{z.get('poles',0)}"
)
return "\n".join(lines)
# ── 플레이어 정보 ───────────────────────────────────────────────
def _get_player_info(self) -> dict:
lua = P + """
local inv = p.get_main_inventory()
if not inv then rcon.print("{}") return end
local contents = inv.get_contents()
local key_items = {
"iron-plate","copper-plate","steel-plate","iron-ore","copper-ore","coal",
"stone","stone-furnace","burner-mining-drill","electric-mining-drill",
"transport-belt","inserter","burner-inserter","fast-inserter",
"small-electric-pole","medium-electric-pole","pipe",
"offshore-pump","boiler","steam-engine","assembling-machine-1",
"assembling-machine-2","lab","iron-gear-wheel",
"automation-science-pack","logistic-science-pack","chemical-science-pack",
"military-science-pack","production-science-pack","utility-science-pack"
}
local inv_summary = {}
for _, name in ipairs(key_items) do
local c = contents[name] or 0
if c > 0 then inv_summary[name] = c end
end
rcon.print(game.table_to_json({
x = math.floor(p.position.x),
y = math.floor(p.position.y),
inventory = inv_summary
}))
"""
raw = self.rcon.lua(lua)
return json.loads(raw) if raw else {}
# ── 포맷터 ──────────────────────────────────────────────────────
def _format_global(self, g: dict, problems: list, player: dict) -> str:
lines = [
"## 공장 상태 (글로벌 요약)",
f"위치: ({player.get('x','?')}, {player.get('y','?')})",
f"전체 건물 수: {g.get('total_entities', 0)}",
f"진화도: {g.get('evolution', 0)}%",
f"완료 연구: {g.get('researched_count', 0)}",
f"진행 중 연구: {g.get('current_research','없음')} ({g.get('research_progress',0)}%)",
]
if problems:
lines.append("\n## 감지된 문제")
for p in problems:
lines.append(f" ! {p}")
lines += self._format_inventory(player.get("inventory", {}))
return "\n".join(lines)
def _format_level1(self, g: dict, zones: list, problems: list, player: dict) -> str:
lines = [self._format_global(g, problems, player), "\n## 구역별 요약"]
for z in sorted(zones, key=lambda x: x.get("entities", 0), reverse=True)[:8]:
role = self._guess_zone_role(z)
lines.append(
f" [{z['x']:+d},{z['y']:+d}] {role} | "
f"채굴{z.get('miners',0)} 제련{z.get('furnaces',0)} "
f"조립{z.get('assemblers',0)} 벨트{z.get('belts',0)}"
)
return "\n".join(lines)
def _format_level2(self, g, zones, problems, drilldown, player) -> str:
base = self._format_level1(g, zones, problems, player)
if drilldown:
base += "\n\n" + drilldown
return base
def _format_inventory(self, inv: dict) -> list[str]:
if not inv:
return ["\n인벤토리: 비어있음"]
lines = ["\n## 인벤토리 (핵심 아이템)"]
for name, count in sorted(inv.items(), key=lambda x: -x[1])[:20]:
lines.append(f" {name}: {count}")
return lines
def _guess_zone_role(self, z: dict) -> str:
miners = z.get("miners", 0)
furnaces = z.get("furnaces", 0)
assemblers= z.get("assemblers", 0)
turrets = z.get("turrets", 0)
labs = z.get("labs", 0)
pumps = z.get("pumps", 0)
if turrets > 3: return "방어구역"
if labs > 0: return "연구실"
if pumps > 0: return "정유/전력"
if assemblers > miners: return "조립라인"
if furnaces > miners: return "제련구역"
if miners > 0: return "채굴구역"
return "기타"