From 55c965750073a975039c4f629a0672728b345a26 Mon Sep 17 00:00:00 2001 From: gihyeon Date: Wed, 25 Mar 2026 10:26:28 +0900 Subject: [PATCH] =?UTF-8?q?Update=20state=5Freader.py=20-=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=9D=BD=EA=B8=B0=20=EB=AA=A8=EB=93=88=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EB=B2=84=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- state_reader.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/state_reader.py b/state_reader.py index 6857bf2..1d0b967 100644 --- a/state_reader.py +++ b/state_reader.py @@ -1 +1,188 @@ -IiIiCnN0YXRlX3JlYWRlci5weQpSQ09O7J2EIO2Gte2VtCDtjKnthqDrpqzsmKQg6rKM7J6EIOyDge2DnOulvCDsnb3slrTsmKTripQg66qo65OICiIiIgppbXBvcnQganNvbgpmcm9tIGZhY3RvcmlvX3Jjb24gaW1wb3J0IEZhY3RvcmlvUkNPTgoKCmNsYXNzIFN0YXRlUmVhZGVyOgogICAgZGVmIF9faW5pdF9fKHNlbGYsIHJjb246IEZhY3RvcmlvUkNPTik6CiAgICAgICAgc2VsZi5yY29uID0gcmNvbgoKICAgIGRlZiBnZXRfZnVsbF9zdGF0ZShzZWxmKSAtPiBkaWN0OgogICAgICAgICIiIuyghOyytCDqsozsnoQg7IOB7YOc66W8IO2VnCDrsojsl5Ag7IiY7KeRIiIiCiAgICAgICAgcmV0dXJuIHsKICAgICAgICAgICAgInBsYXllciI6ICAgIHNlbGYuZ2V0X3BsYXllcl9pbmZvKCksCiAgICAgICAgICAgICJpbnZlbnRvcnkiOiBzZWxmLmdldF9pbnZlbnRvcnkoKSwKICAgICAgICAgICAgInJlc291cmNlcyI6IHNlbGYuc2Nhbl9yZXNvdXJjZXMoKSwKICAgICAgICAgICAgImJ1aWxkaW5ncyI6IHNlbGYuZ2V0X2J1aWxkaW5ncygpLAogICAgICAgICAgICAidGVjaCI6ICAgICAgc2VsZi5nZXRfcmVzZWFyY2hfc3RhdHVzKCksCiAgICAgICAgfQoKICAgICMg4pSA4pSAIO2UjOugiOydtOyWtCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgIGRlZiBnZXRfcGxheWVyX2luZm8oc2VsZikgLT4gZGljdDoKICAgICAgICBsdWEgPSAiIiIKbG9jYWwgcCA9IGdhbWUucGxheWVyCnJjb24ucHJpbnQoZ2FtZS50YWJsZV90b19qc29uKHsKICB4ID0gbWF0aC5mbG9vcihwLnBvc2l0aW9uLngpLAogIHkgPSBtYXRoLmZsb29yKHAucG9zaXRpb24ueSksCiAgaGVhbHRoID0gcC5jaGFyYWN0ZXIgYW5kIHAuY2hhcmFjdGVyLmhlYWx0aCBvciAxMDAKfSkpCiIiIgogICAgICAgIHJhdyA9IHNlbGYucmNvbi5sdWEobHVhKQogICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJhdykgaWYgcmF3IGVsc2Uge30KCiAgICAjIOKUgOKUgCDsnbjrsqTthqDrpqwg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICBkZWYgZ2V0X2ludmVudG9yeShzZWxmKSAtPiBkaWN0OgogICAgICAgIGx1YSA9ICIiIgpsb2NhbCBpbnYgPSBnYW1lLnBsYXllci5nZXRfbWFpbl9pbnZlbnRvcnkoKQpsb2NhbCByZXN1bHQgPSB7fQpsb2NhbCBjb250ZW50cyA9IGludi5nZXRfY29udGVudHMoKQpmb3IgbmFtZSwgY291bnQgaW4gcGFpcnMoY29udGVudHMpIGRvCiAgcmVzdWx0W25hbWVdID0gY291bnQKZW5kCnJjb24ucHJpbnQoZ2FtZS50YWJsZV90b19qc29uKHJlc3VsdCkpCiIiIgogICAgICAgIHJhdyA9IHNlbGYucmNvbi5sdWEobHVhKQogICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJhdykgaWYgcmF3IGVsc2Uge30KCiAgICAjIOKUgOKUgCDsnpDsm5Ag7Yyo7LmYIOyKpOy6lCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgIGRlZiBzY2FuX3Jlc291cmNlcyhzZWxmLCByYWRpdXM6IGludCA9IDIwMCkgLT4gZGljdDoKICAgICAgICAiIiLtlIzroIjsnbTslrQg7KO867OAIOyekOybkCDtjKjsuZgg7JyE7LmYIO2DkOyDiSIiIgogICAgICAgIGx1YSA9IGYiIiIKbG9jYWwgcCA9IGdhbWUucGxheWVyCmxvY2FsIHN1cmZhY2UgPSBwLnN1cmZhY2UKbG9jYWwgY2VudGVyID0gcC5wb3NpdGlvbgpsb2NhbCByZXNvdXJjZXMgPSB7e319Cgpsb2NhbCBvcmVfdHlwZXMgPSB7eyJpcm9uLW9yZSIsICJjb3BwZXItb3JlIiwgImNvYWwiLCAic3RvbmUiLCAidXJhbml1bS1vcmUifX0KZm9yIF8sIG9yZSBpbiBpcGFpcnMob3JlX3R5cGVzKSBkbwogIGxvY2FsIGVudGl0aWVzID0gc3VyZmFjZS5maW5kX2VudGl0aWVzX2ZpbHRlcmVke3sKICAgIGFyZWEgPSB7ewogICAgICB7e2NlbnRlci54IC0ge3JhZGl1c30sIGNlbnRlci55IC0ge3JhZGl1c319fSwKICAgICAge3tjZW50ZXIueCArIHtyYWRpdXN9LCBjZW50ZXIueSArIHtyYWRpdXN9fX0KICAgIH19LAogICAgbmFtZSA9IG9yZQogIH19CiAgaWYgI2VudGl0aWVzID4gMCB0aGVuCiAgICAtLSDtjKjsuZgg7KSR7Ius7KCQIOqzhOyCsAogICAgbG9jYWwgc3gsIHN5ID0gMCwgMAogICAgZm9yIF8sIGUgaW4gaXBhaXJzKGVudGl0aWVzKSBkbwogICAgICBzeCA9IHN4ICsgZS5wb3NpdGlvbi54CiAgICAgIHN5ID0gc3kgKyBlLnBvc2l0aW9uLnkKICAgIGVuZAogICAgcmVzb3VyY2VzW29yZV0gPSB7ewogICAgICBjb3VudCA9ICNlbnRpdGllcywKICAgICAgY2VudGVyX3ggPSBtYXRoLmZsb29yKHN4IC8gI2VudGl0aWVzKSwKICAgICAgY2VudGVyX3kgPSBtYXRoLmZsb29yKHN5IC8gI2VudGl0aWVzKQogICAgfX0KICBlbmQKZW5kCnJjb24ucHJpbnQoZ2FtZS50YWJsZV90b19qc29uKHJlc291cmNlcykpCiIiIgogICAgICAgIHJhdyA9IHNlbGYucmNvbi5sdWEobHVhKQogICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJhdykgaWYgcmF3IGVsc2Uge30KCiAgICAjIOKUgOKUgCDqsbTrrLwg66qp66GdIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAogICAgZGVmIGdldF9idWlsZGluZ3Moc2VsZiwgcmFkaXVzOiBpbnQgPSAzMDApIC0+IGxpc3Q6CiAgICAgICAgbHVhID0gZiIiIgpsb2NhbCBwID0gZ2FtZS5wbGF5ZXIKbG9jYWwgc3VyZmFjZSA9IHAuc3VyZmFjZQpsb2NhbCBjZW50ZXIgPSBwLnBvc2l0aW9uCgpsb2NhbCBlbnRpdHlfbmFtZXMgPSB7ewogICJidXJuZXItbWluaW5nLWRyaWxsIiwgImVsZWN0cmljLW1pbmluZy1kcmlsbCIsCiAgInN0b25lLWZ1cm5hY2UiLCAic3RlZWwtZnVybmFjZSIsICJlbGVjdHJpYy1mdXJuYWNlIiwKICAidHJhbnNwb3J0LWJlbHQiLCAiZmFzdC10cmFuc3BvcnQtYmVsdCIsCiAgImJ1cm5lci1pbnNlcnRlciIsICJpbnNlcnRlciIsCiAgInNtYWxsLWVsZWN0cmljLXBvbGUiLCAibWVkaXVtLWVsZWN0cmljLXBvbGUiLAogICJzdGVhbS1lbmdpbmUiLCAiYm9pbGVyIiwgIm9mZnNob3JlLXB1bXAiLAogICJhc3NlbWJsaW5nLW1hY2hpbmUtMSIsICJhc3NlbWJsaW5nLW1hY2hpbmUtMiIsCiAgImNoZXN0IiwgImlyb24tY2hlc3QiLCAic3RlZWwtY2hlc3QiCn19Cgpsb2NhbCByZXN1bHQgPSB7e319CmZvciBfLCBuYW1lIGluIGlwYWlycyhlbnRpdHlfbmFtZXMpIGRvCiAgbG9jYWwgZW50aXRpZXMgPSBzdXJmYWNlLmZpbmRfZW50aXRpZXNfZmlsdGVyZWR7ewogICAgYXJlYSA9IHt7CiAgICAgIHt7Y2VudGVyLnggLSB7cmFkaXVzfSwgY2VudGVyLnkgLSB7cmFkaXVzfX19LAogICAgICB7e2NlbnRlci54ICsge3JhZGl1c30sIGNlbnRlci55ICsge3JhZGl1c319fQogICAgfX0sCiAgICBuYW1lID0gbmFtZQogIH19CiAgaWYgI2VudGl0aWVzID4gMCB0aGVuCiAgICByZXN1bHRbbmFtZV0gPSAjZW50aXRpZXMKICBlbmQKZW5kCnJjb24ucHJpbnQoZ2FtZS50YWJsZV90b19qc29uKHJlc3VsdCkpCiIiIgogICAgICAgIHJhdyA9IHNlbGYucmNvbi5sdWEobHVhKQogICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJhdykgaWYgcmF3IGVsc2Uge30KCiAgICAjIOKUgOKUgCDsl7Dqtawg7ZiE7ZmpIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAogICAgZGVmIGdldF9yZXNlYXJjaF9zdGF0dXMoc2VsZikgLT4gZGljdDoKICAgICAgICBsdWEgPSAiIiIKbG9jYWwgZm9yY2UgPSBnYW1lLnBsYXllci5mb3JjZQpsb2NhbCBjb21wbGV0ZWQgPSB7fQpsb2NhbCBrID0gMApmb3IgbmFtZSwgdGVjaCBpbiBwYWlycyhmb3JjZS50ZWNobm9sb2dpZXMpIGRvCiAgaWYgdGVjaC5yZXNlYXJjaGVkIHRoZW4KICAgIGsgPSBrICsgMQogICAgY29tcGxldGVkW2tdID0gbmFtZQogIGVuZAplbmQKcmNvbi5wcmludChnYW1lLnRhYmxlX3RvX2pzb24oewogIGN1cnJlbnQgPSBmb3JjZS5jdXJyZW50X3Jlc2VhcmNoIGFuZCBmb3JjZS5jdXJyZW50X3Jlc2VhcmNoLm5hbWUgb3IgIm5vbmUiLAogIGNvbXBsZXRlZF9jb3VudCA9IGsKfSkpCiIiIgogICAgICAgIHJhdyA9IHNlbGYucmNvbi5sdWEobHVhKQogICAgICAgIHJldHVybiBqc29uLmxvYWRzKHJhdykgaWYgcmF3IGVsc2Uge30KCiAgICBkZWYgc3VtbWFyaXplX2Zvcl9haShzZWxmLCBzdGF0ZTogZGljdCkgLT4gc3RyOgogICAgICAgICIiIkFJIO2UhOuhrO2UhO2KuOyaqSDsg4Htg5wg7JqU7JW9IO2FjeyKpO2KuCDsg53shLEiIiIKICAgICAgICBwICAgPSBzdGF0ZS5nZXQoInBsYXllciIsIHt9KQogICAgICAgIGludiA9IHN0YXRlLmdldCgiaW52ZW50b3J5Iiwge30pCiAgICAgICAgcmVzID0gc3RhdGUuZ2V0KCJyZXNvdXJjZXMiLCB7fSkKICAgICAgICBibGQgPSBzdGF0ZS5nZXQoImJ1aWxkaW5ncyIsIHt9KQoKICAgICAgICBsaW5lcyA9IFsKICAgICAgICAgICAgZiIjIyDtmITsnqwg6rKM7J6EIOyDge2DnCIsCiAgICAgICAgICAgIGYiIyMjIO2UjOugiOydtOyWtCIsCiAgICAgICAgICAgIGYiLSDsnITsuZg6ICh7cC5nZXQoJ3gnLCAnPycpfSwge3AuZ2V0KCd5JywgJz8nKX0pIiwKICAgICAgICAgICAgZiItIOyytOugpToge3AuZ2V0KCdoZWFsdGgnLCAnPycpfSIsCiAgICAgICAgICAgICIiLAogICAgICAgICAgICAiIyMjIOyduOuypO2GoOumrCAo7KO87JqUIOyVhOydtO2FnCkiLAogICAgICAgIF0KCiAgICAgICAga2V5X2l0ZW1zID0gWwogICAgICAgICAgICAiaXJvbi1vcmUiLCAiY29wcGVyLW9yZSIsICJjb2FsIiwgInN0b25lIiwKICAgICAgICAgICAgImlyb24tcGxhdGUiLCAiY29wcGVyLXBsYXRlIiwgInN0ZWVsLXBsYXRlIiwKICAgICAgICAgICAgImJ1cm5lci1taW5pbmctZHJpbGwiLCAiZWxlY3RyaWMtbWluaW5nLWRyaWxsIiwKICAgICAgICAgICAgInN0b25lLWZ1cm5hY2UiLCAidHJhbnNwb3J0LWJlbHQiLCAiaW5zZXJ0ZXIiLAogICAgICAgICAgICAiYnVybmVyLWluc2VydGVyIiwgInNtYWxsLWVsZWN0cmljLXBvbGUiLAogICAgICAgIF0KICAgICAgICBmb3IgaXRlbSBpbiBrZXlfaXRlbXM6CiAgICAgICAgICAgIGNvdW50ID0gaW52LmdldChpdGVtLCAwKQogICAgICAgICAgICBpZiBjb3VudCA+IDA6CiAgICAgICAgICAgICAgICBsaW5lcy5hcHBlbmQoZiItIHtpdGVtfToge2NvdW50feqwnCIpCgogICAgICAgIGxpbmVzICs9IFsiIiwgIiMjIyDso7zrs4Ag7J6Q7JuQIO2MqOy5mCJdCiAgICAgICAgaWYgcmVzOgogICAgICAgICAgICBmb3Igb3JlLCBpbmZvIGluIHJlcy5pdGVtcygpOgogICAgICAgICAgICAgICAgbGluZXMuYXBwZW5kKAogICAgICAgICAgICAgICAgICAgIGYiLSB7b3JlfToge2luZm9bJ2NvdW50J1197YOA7J28ICIKICAgICAgICAgICAgICAgICAgICBmIijspJHsi6w6IHtpbmZvWydjZW50ZXJfeCddfSwge2luZm9bJ2NlbnRlcl95J119KSIKICAgICAgICAgICAgICAgICkKICAgICAgICBlbHNlOgogICAgICAgICAgICBsaW5lcy5hcHBlbmQoIi0g7YOQ7IOJ65CcIOyekOybkCDsl4bsnYwiKQoKICAgICAgICBsaW5lcyArPSBbIiIsICIjIyMg6rG07ISk65CcIOqxtOusvCJdCiAgICAgICAgaWYgYmxkOgogICAgICAgICAgICBmb3IgbmFtZSwgY291bnQgaW4gYmxkLml0ZW1zKCk6CiAgICAgICAgICAgICAgICBsaW5lcy5hcHBlbmQoZiItIHtuYW1lfToge2NvdW50feqwnCIpCiAgICAgICAgZWxzZToKICAgICAgICAgICAgbGluZXMuYXBwZW5kKCItIOyVhOyngSDqsbTrrLwg7JeG7J2MIikKCiAgICAgICAgcmV0dXJuICJcbiIuam9pbihsaW5lcykK \ No newline at end of file +""" +state_reader.py +RCON을 통해 팩토리오 게임 상태를 읽어오는 모듈 +""" +import json +from factorio_rcon import FactorioRCON + + +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 = """ +local p = game.player +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 {} + + # ── 인벤토리 ──────────────────────────────────────────────── + def get_inventory(self) -> dict: + lua = """ +local inv = game.player.get_main_inventory() +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 = 200) -> dict: + """플레이어 주변 자원 패치 위치 탐색""" + lua = f""" +local p = game.player +local surface = p.surface +local center = p.position +local resources = {{}} + +local ore_types = {{"iron-ore", "copper-ore", "coal", "stone", "uranium-ore"}} +for _, ore in ipairs(ore_types) do + local entities = surface.find_entities_filtered{{ + area = {{ + {{center.x - {radius}, center.y - {radius}}}, + {{center.x + {radius}, center.y + {radius}}} + }}, + name = ore + }} + if #entities > 0 then + -- 패치 중심점 계산 + local sx, sy = 0, 0 + for _, e in ipairs(entities) do + sx = sx + e.position.x + sy = sy + e.position.y + end + resources[ore] = {{ + count = #entities, + center_x = math.floor(sx / #entities), + center_y = math.floor(sy / #entities) + }} + end +end +rcon.print(game.table_to_json(resources)) +""" + raw = self.rcon.lua(lua) + return json.loads(raw) if raw else {} + + # ── 건물 목록 ─────────────────────────────────────────────── + def get_buildings(self, radius: int = 300) -> list: + lua = f""" +local p = game.player +local surface = p.surface +local center = p.position + +local entity_names = {{ + "burner-mining-drill", "electric-mining-drill", + "stone-furnace", "steel-furnace", "electric-furnace", + "transport-belt", "fast-transport-belt", + "burner-inserter", "inserter", + "small-electric-pole", "medium-electric-pole", + "steam-engine", "boiler", "offshore-pump", + "assembling-machine-1", "assembling-machine-2", + "chest", "iron-chest", "steel-chest" +}} + +local result = {{}} +for _, name in ipairs(entity_names) do + local entities = surface.find_entities_filtered{{ + area = {{ + {{center.x - {radius}, center.y - {radius}}}, + {{center.x + {radius}, center.y + {radius}}} + }}, + name = name + }} + if #entities > 0 then + result[name] = #entities + end +end +rcon.print(game.table_to_json(result)) +""" + raw = self.rcon.lua(lua) + return json.loads(raw) if raw else {} + + # ── 연구 현황 ─────────────────────────────────────────────── + def get_research_status(self) -> dict: + lua = """ +local force = game.player.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 +})) +""" + raw = self.rcon.lua(lua) + return json.loads(raw) if raw else {} + + def summarize_for_ai(self, state: dict) -> str: + """AI 프롬프트용 상태 요약 텍스트 생성""" + p = state.get("player", {}) + inv = state.get("inventory", {}) + res = state.get("resources", {}) + bld = state.get("buildings", {}) + + lines = [ + f"## 현재 게임 상태", + f"### 플레이어", + 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: + lines.append(f"- {item}: {count}개") + + lines += ["", "### 주변 자원 패치"] + if res: + for ore, info in res.items(): + lines.append( + f"- {ore}: {info['count']}타일 " + f"(중심: {info['center_x']}, {info['center_y']})" + ) + else: + lines.append("- 탐색된 자원 없음") + + lines += ["", "### 건설된 건물"] + if bld: + for name, count in bld.items(): + lines.append(f"- {name}: {count}개") + else: + lines.append("- 아직 건물 없음") + + return "\n".join(lines)