# 팩토리오 AI 에이전트 (순수 플레이) AI가 팩토리오를 **치트 없이** 자율적으로 플레이하는 에이전트입니다. 실제 걷기, 실제 채굴, 실제 제작, 건설 거리 제한 등 모든 게임 메커닉을 준수합니다. --- ## 순수 플레이 모드란? | 항목 | 치트 모드 | 순수 모드 (현재) | |------|----------|-----------------| | 이동 | 텔레포트 | 실제 걷기 (walking_state) | | 채굴 | 인벤토리 직접 삽입 | 자원 패치에서 실제 채굴 (mining_state) | | 제작 | 무조건 지급 | 재료 소모 실제 제작 (begin_crafting) | | 건설 | 어디서든 create_entity | 건설 거리 내에서 build_from_cursor | | 삽입 | 무한 아이템 | 플레이어 인벤토리에서 차감 | --- ## 파일 구조 ``` factorio_ai/ ├── main.py ← 메인 루프 (여기서 실행) ├── factorio_rcon.py ← RCON 연결 (게임과 통신) ├── state_reader.py ← 게임 상태 읽기 (자원, 인벤토리, 건물) ├── context_compressor.py ← 중반 이후 상태 압축 ├── ai_planner.py ← AI 행동 계획 (순수 플레이 제약 반영) ├── action_executor.py ← 실제 게임 조작 (순수 메커닉) └── agent_log.jsonl ← 행동 로그 (자동 생성) ``` --- ## 설치 ```bash # 외부 라이브러리 불필요! Python 표준 라이브러리만 사용 ``` --- ## 팩토리오 RCON 설정 ### 방법 1: 싱글플레이 1. 팩토리오 실행 2. 게임 시작 후 콘솔 열기 (` 키 또는 ~) 3. 아래 입력: ``` /rcon-port 25575 /rcon-password factorio_ai ``` ### 방법 2: 서버 모드 (권장) ```bash # Windows factorio.exe --start-server saves/mysave.zip --rcon-port 25575 --rcon-password factorio_ai # Linux/Mac ./factorio --start-server saves/mysave.zip --rcon-port 25575 --rcon-password factorio_ai ``` --- ## 실행 ```bash # Z.ai API 키 설정 export ZAI_API_KEY="your-key-here" # 기본 실행 python main.py # 커스텀 서버 FACTORIO_HOST=192.168.1.10 FACTORIO_PORT=25575 FACTORIO_PASSWORD=mypass python main.py ``` ```powershell # Z.ai API 키 설정 $env:ZAI_API_KEY="your-key-here" # 기본 실행 python .\main.py # 커스텀 서버 (한 줄 버전) $env:FACTORIO_HOST="192.168.1.10"; $env:FACTORIO_PORT="25575"; $env:FACTORIO_PASSWORD="mypass"; python .\main.py # (권장) 스크립트 실행 .\run_factorio_ai.ps1 "your-key-here" "mypass" ``` --- ## 동작 원리 ``` 1. RCON → 팩토리오에서 현재 상태 읽기 (플레이어 위치, 인벤토리, 자원 패치, 건물 목록) 2. 상태 → AI API로 전송 "다음에 뭘 해야 하나요?" (순수 플레이 제약 포함) 3. AI → JSON 행동 시퀀스 반환 [move → mine_resource → craft_item → place_entity ...] 4. RCON → 팩토리오에서 실제 게임 메커닉으로 실행 - 캐릭터가 실제로 걸어감 - 실제로 곡괭이질해서 채굴 - 재료를 소모해서 제작 - 건설 거리 내에서만 배치 ``` --- ## AI 목표 변경 ```python planner.set_goal( "전력 인프라 구축: offshore-pump → boiler → steam-engine → 전선 연결" ) ``` --- ## GLM API 연결 문제 디버깅 타임아웃·연결 오류가 나면 **원인을 로그로 먼저 구분**하는 것이 좋습니다. 1. **전용 검사 스크립트** (에이전트와 동일한 `urllib` 경로): ```bash ZAI_API_KEY="your-key" python scripts/glm_connection_check.py ``` 2. **실행 시 상세 로그**: `GLM_DEBUG=1`을 켜면 재시도마다 `[GLM 원인] …` 한 줄에 `URLError.reason`, SSL/소켓 `errno`, DNS 힌트 등이 붙고, 스택 트레이스도 출력됩니다. 3. **자주 있는 원인**: Docker/서버에서 **외부 HTTPS(443) 차단**, **프록시 필요**( `HTTPS_PROXY` ), **DNS 실패**, **API 키 만료·오타**, **응답이 느려 타임아웃**( `GLM_HTTP_TIMEOUT_SECONDS` 증가 ). --- ## 주의사항 - 순수 플레이이므로 **걷기, 채굴, 제작에 실제 시간이 소요**됩니다 - AI가 "move 먼저 → 작업" 패턴을 학습하도록 프롬프트가 설계되어 있습니다 - `ai_planner.py`의 Ollama/LM Studio 호출은 서버 스키마 차이를 자동으로 맞추기 위해 payload 후보를 순차 재시도합니다. 먼저 `messages` 기반(legacy chat)으로 시도하고, 400(`unrecognized_keys`/`input required`)이면 `input` 중심 최소 payload로 재시도합니다. - LM Studio `input-only` 모드에서 `output[].type=reasoning` 형태로 응답이 오는 경우도 파서가 처리하며, 응답 텍스트에 JSON 앞뒤 설명이 섞이면 `{...}` 구간을 복구 추출해 파싱을 시도합니다(복구 불가 시 기존 휴리스틱 폴백). - 파싱된 계획은 `actions` 스키마와 지원 action 화이트리스트를 통과한 경우에만 채택합니다. 유효 액션이 없거나 형식이 깨지면 LLM 출력을 버리고 상태 기반 휴리스틱 폴백으로 전환해 `알 수 없는 행동` 루프를 줄입니다. - LM Studio가 표준과 다른 동의어를 반환할 때(예: `place_building`, `target_x/target_y`, `position_x/position_y`), planner가 표준 스키마(`place_entity`, `x/y`, `name`)로 자동 정규화한 뒤 검증합니다. - 특히 `mine_resource`는 실행기 시그니처에 맞게 `ore`, `count`만 남기도록 강제 정규화합니다. `resource`/`item`은 `ore`로 매핑하고 `tile`/`target` 같은 비표준 키는 제거해 `unexpected keyword argument` 오류를 방지합니다. - `ai_planner.py`는 LLM이 move 순서를 놓쳐 `place_entity`/`insert_to_entity`/`set_recipe`가 건설 거리 제한으로 실패하는 경우를 줄이기 위해, 해당 액션 직전에 최근 `move`가 같은 좌표가 아니면 자동으로 `move`를 끼워 넣습니다 - `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다 - `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다. 또한 최상위에서 JSON 배열(`[...]`)로 답하는 경우도 `actions`로 래핑해 처리합니다. (추가) `finish_reason=length` 등으로 `message.content`가 비거나(또는 JSON 형태가 아니면) `message.reasoning_content`를 우선 사용합니다. 그리고 응답 안에 여러 개의 `{...}`가 섞여 있어도 그중 `actions`를 포함한 계획 객체를 우선 선택합니다. 또한 JSON 파싱 실패가 감지되면 다음 재시도에는 `JSON-only repair` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다. - 재시도(attempt>0)에서는 `max_tokens`를 줄여(기본 `900`) 분석 텍스트가 길어져 JSON이 잘리는 패턴을 줄입니다. - `JSON-only repair` 프롬프트가 붙는 경우에는 `max_tokens`를 더 낮추고(기본 `250`) `temperature`도 더 낮춰(기본 `0.0`) JSON 포맷 준수율을 올리려 요청합니다. - `JSON-only repair`를 한 번 적용했는데도 비JSON 텍스트가 계속 오면, 불필요한 API 재시도 반복 대신 즉시 상태 기반 휴리스틱 폴백으로 전환합니다. 또한 LLM이 `actions=[]`를 반환하면 같은 이유로 휴리스틱 플랜으로 즉시 대체합니다. - `finish_reason=length`이면서 응답이 비JSON 텍스트(`{' 없음`)로 시작하면, repair 재시도를 기다리지 않고 즉시 휴리스틱 폴백으로 전환해 턴 지연을 줄입니다. - `ai_planner.py`의 `AIPlanner.decide()`는 `TimeoutError`/`ConnectionError`/`urllib.error.URLError` 같은 GLM HTTP 지연/연결 오류도 3회 재시도한 뒤, **상태 요약에 나온 광맥(앵커) 좌표가 있으면 `mine_resource`(먼 경우 `move` 후 채굴)로 폴백**하고, 광맥 정보가 없을 때만 `explore` 방향을 순환하며 탐색합니다(동일 방향 탐색 루프 완화). - GLM이 `HTTP 429 (Rate limit)`에 걸리면 `Retry-After`가 있으면 그만큼 기다렸다가(없으면 기본 백오프) 재시도하도록 처리합니다. - GLM HTTP 읽기 제한 시간은 기본 120초이며, `GLM_HTTP_TIMEOUT_SECONDS`로 조정할 수 있습니다. 광맥은 플레이어와 200타일 이상 떨어진 경우에만 폴백에서 `move`를 끼우며, 임계값은 `GLM_FALLBACK_MOVE_THRESHOLD`(기본 200)로 바꿀 수 있습니다. - `ai_planner.py`의 `_call_glm()`에서 GLM 지연 원인 분석을 위한 타이밍 로그가 출력됩니다. - `total`: 요청 시작~콘텐츠 반환까지 전체 소요 - `http_read`: HTTP 응답 본문 수신까지 소요 - `json_parse`: 응답 JSON 파싱 시간 - 파이썬 에이전트 재실행 시 인벤토리 “기억”이 필요하면 `inventory_memory.json` 캐시를 사용합니다. - `RCON`에서 인벤토리를 읽지 못하거나(비정상 출력/파싱 실패 등) 직전에 성공적으로 읽은 인벤토리를 대신 사용합니다. - 성공적으로 인벤토리가 읽히면 해당 캐시를 갱신합니다(코드는 `inv.get_contents()`가 `nil`/비테이블을 반환해도 안전하게 처리). - 또한 `inv.get_contents()` 결과가 비면 Lua에서 인덱스 기반으로 한 번 더 읽어서 `{}`가 과도하게 나오는 걸 줄입니다. - 캐시 대체는 `get_inventory()`의 파싱이 실패한 경우에만 수행합니다. - 또한 일부 환경에서 `p.get_main_inventory()`가 없으면 `defines.inventory.character_main`으로 한 번 더 읽습니다. - 캐시 파일이 아직 없으면(초기 실행) fallback도 `{}`가 됩니다. - (중요) 일부 Factorio 버전에서는 `game.table_to_json`이 없을 수 있어서, Lua 내부에 간단 JSON 인코더를 넣어 인벤토리/상태를 안정적으로 가져옵니다. - `explore` 및 `scan_resources()`로 발견한 광맥(자원 엔티티) 좌표는 `ore_patch_memory.json`에 ore 종류별로 여러 패치 좌표 목록으로 저장되고, 다음 상태 요약에서 AI가 좌표를 재사용(우선 이동)할 수 있게 합니다. - 디버깅용으로 `INV_DEBUG=1`을 켜면, `main inventory` 외에 `cursor_stack`/`armor`/`trash` 총합도 함께 출력합니다(어디에 아이템이 있는지 확인용). - `mine_resource`에서 실패한 채굴 타일 제외(`exclude`)는 Lua와 Python 양쪽에서 정수 타일 좌표(`tx, ty`) 키로 통일해, 제외한 좌표가 반복 선택되지 않도록 합니다. - 또한 채굴 시작(`mining_state`) 좌표는 정수 타일이 아니라, Lua가 찾은 실제 자원 엔티티의 `e.position`(정확 실수 좌표)을 사용해 “플레이어가 타일 위에 있는데도 즉시 채굴 감지 실패”를 줄입니다. - `mine_resource`는 `LuaControl.mining_state`를 **해당 타깃에 대해 한 번만** 설정합니다. 이전에는 대기 루프마다 같은 값을반복 설정해 우클릭을 연타하는 것과 비슷한 동작이었으며, 이제는 인간이 우클릭을 누른 채로 두는 것에 가깝게 유지합니다. - `mine_resource`는 move 후 `p.position` 근처(반경 1.2)에서 실제로 해당 광물 엔티티가 있는지 재확인하고, 없으면 채굴을 시도하지 않고 다음 후보로 넘어갑니다. - `scan_resources()`는 패치 평균 중심(`center_x/y`)만이 아니라 플레이어 기준 가장 가까운 실제 엔티티 좌표(`anchor_x/y`)도 함께 반환하며, `summarize_for_ai()`에서는 앵커를 기준으로 거리/추천을 계산합니다. - `mine_resource`의 초기 “후보 엔티티 탐색” 반경은 패치 중심 오차를 흡수하기 위해 250으로 확장해, 이동 직후 바로 실패하는 케이스를 줄입니다. - 에이전트를 재시작하더라도 직전에 실행했던 action/result를 `agent_last_action_memory.json`에 저장해, 다음 상태 요약에서 AI가 참고할 수 있게 합니다. - 저장 기준: 성공/실패 상관없이 가장 마지막으로 실행을 시도한 action 1개만 저장합니다. - `explore`는 `wanted_ores`가 있으면 해당 자원이 발견될 때까지 멈추지 않고 계속 이동해, `iron-ore`처럼 주변에 흔한 자원만 계속 발견되어 진행이 막히는 문제를 줄입니다. - `place_entity`는 지정 좌표가 `BLOCKED`이거나 인벤토리에 해당 아이템이 없어도(예: 이미 설치됨) 주변 `±1 타일` 및 요청 좌표 중심 반경 `3` 타일 내의 기존 엔티티를 우선 찾아 `REUSED`로 재사용합니다. - `stone-furnace`는 `place_entity`가 성공하면(신규 배치든 `REUSED`든) executor가 즉시 `coal`(연료)와 `iron-ore`/`copper-ore`(가능한 것 우선)를 투입해 제련이 시작되도록 보정합니다. - 또한 `insert_to_entity`는 burner 인벤토리 투입 전에 `can_insert`를 확인해, 미삽입/아이템 증발 위험을 줄입니다. - (Cursor) Windows에서 `sessionStart` 훅 실행 중 앱 선택창이 뜨는 경우: - 프로젝트 훅은 `E:/develop/factorio-ai-agent/.cursor/hooks.json` 및 `E:/develop/factorio-ai-agent/.cursor/session-start-hook.ps1`를 PowerShell로 실행하도록 구성되어 있습니다. - Superpowers 플러그인의 `./hooks/session-start`가 bash로 실행되도록 `hooks-cursor.json`을 수정했습니다(필요 시 Cursor 재시작 후 `View -> Output -> Hooks`에서 확인). - 인벤토리 판독은 `inv.get_contents()`를 우선 사용해, 일부 환경에서 `#inv` / 인덱스 접근이 비어있음으로 오인되는 문제를 줄입니다.