""" 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 "기타"