# War3AI / OpenWar3 전체 문서
> 출처 https://war3ai.com/ko . AI Agent를 위한 워크래프트 III 1.27 오픈 인터페이스. Bot을 작성할 때는 문서 끝의 "API 카탈로그"에 나열된 Game 메서드만 사용할 수 있습니다.
---
# 문서 개요
> OpenWar3 문서: 개요와 기능, 빠른 시작·첫 번째 Bot·LLM Bot·API·프로토콜·게이트웨이·MCP, 상황별 시작 페이지 안내.
**OpenWar3**는 War3AI의 오픈 인터페이스 계층입니다. 워크래프트 III 1.27에 주입되는 런타임과 Python SDK로 구성됩니다.
- 런타임은 **50 ms**마다 맵 전체의 완전한 상태를 공유 메모리에 푸시합니다. 모든 플레이어의 자원과 식량, 모든 유닛의 체력/마나, 오더, 현재 공격 대상, 스킬 쿨다운, 버프, 인벤토리, 바닥의 아이템, 나무, 생산 대기열, 낮과 밤까지 담깁니다. **이벤트 스트림**도 있습니다. 유닛 등장과 사망, 피해 한 번 한 번, 생산 완료……
- 외부 프로그램은 **약 1프레임** 지연으로 **시맨틱 명령**을 내립니다. 이동, 공격, 채집, 건설, 훈련, 시전, 스킬 습득, 부활, 아이템 사용, 구매…… 모든 명령에는 **회신**이 있어 엔진이 수락했는지, 거부했다면 사유 코드가 무엇인지 알려 줍니다.
- 여러분은 "무엇을 할지"만 말하면 됩니다. 유닛은 4자리 코드로, 스킬은 오더 이름으로 지정하며 게임 속 명칭과 같습니다. "어떻게 할지"는 런타임이 맡습니다.
따라서 LLM에는 저수준 지식도, 화면을 보는 능력도 필요 없습니다. 문서를 읽고 나면 운영도 하고 전투도 하는 Bot을 작성할 수 있고, 대전에 나간 뒤에는 회신과 이벤트를 보고 스스로 수정합니다.
대전만이 아닙니다. [캔버스](https://war3ai.com/ko/docs/canvas/)로 게임 화면 위에 나만의 패널과 표시를 그릴 수 있고, [UI와 입력](https://war3ai.com/ko/docs/ui-input/)을 쓰면 화면에 그린 버튼을 클릭할 수 있고 단축키도 반응하며, [JASS 채널](https://war3ai.com/ko/docs/jass/)로 게임 속 함수 1291개를 외부에서 호출할 수 있고, RPG 맵에서는 나만의 [AI 동료](https://war3ai.com/ko/docs/companion/)를 곁에 둘 수도 있습니다. 완성한 AI는 [스킴](https://war3ai.com/ko/docs/schemes/)으로 만들어 클릭 한 번으로 전환하고, 내보내 공유할 수 있습니다. 새로운 게임 방식 하나를 통째로 [게임플레이 모드](https://war3ai.com/ko/docs/mods/)로 작성할 수도 있습니다.
Python을 쓰지 않고도 연결할 수 있습니다. [게이트웨이](https://war3ai.com/ko/docs/gateway/)를 쓰면 어떤 언어든, 브라우저 페이지든 WebSocket / JSON으로 같은 API를 호출할 수 있고, [MCP 서버](https://war3ai.com/ko/docs/mcp/)를 쓰면 Claude Code 같은 Agent가 도구를 직접 호출해 게임 상황을 보고 명령을 내립니다.
- [빠른 시작](https://war3ai.com/ko/docs/quickstart/): 환경을 설치하고 명령 한 줄로 게임을 띄워 예제 Bot이 조종하는 모습을 확인합니다.
- [LLM으로 Bot 만들기](https://war3ai.com/ko/docs/ai-bot/): 프로그래밍을 몰라도 됩니다. 프롬프트를 복사하고 전략을 설명한 뒤 Agent에게 맡기세요.
- [멘탈 모델](https://war3ai.com/ko/docs/concepts/): 스냅샷, 명령, 회신, 이벤트, 틱. Bot을 작성하기 전에 5분만 투자해 읽어 보세요.
- [API 카탈로그](https://war3ai.com/ko/api/): 전체 API. 모두 실측 상태, 지연 등급, 내부 메커니즘이 표시되어 있습니다.
## 상황에 맞는 경로 고르기
| 여러분은 | 여기부터 읽기 | 그다음 |
|---|---|---|
| 워크래프트는 할 줄 알지만 프로그래밍은 모름 | [빠른 시작](https://war3ai.com/ko/docs/quickstart/) → [LLM으로 Bot 만들기](https://war3ai.com/ko/docs/ai-bot/) | 문제가 생기면 [자주 묻는 질문](https://war3ai.com/ko/docs/faq/) |
| Python을 다룰 줄 앎 | [첫 번째 Bot](https://war3ai.com/ko/docs/first-bot/) → [멘탈 모델](https://war3ai.com/ko/docs/concepts/) → [15가지 규칙](https://war3ai.com/ko/docs/rules/) | [프로 전략 레시피](https://war3ai.com/ko/docs/cookbook/), [예제 Bot](https://war3ai.com/ko/docs/examples/) |
| Coding Agent / 자동화를 만드는 중 | [Agent 자율 반복](https://war3ai.com/ko/docs/agent-loop/) | [회신과 사유 코드](https://war3ai.com/ko/docs/reason-codes/), [`llms-full.txt`](https://war3ai.com/ko/llms-full.txt) |
| LLM이 게임 중에 의사결정을 하게 하고 싶음 | [LLM 참모](https://war3ai.com/ko/docs/llm-coach/) | [말풍선과 로컬 모델](https://war3ai.com/ko/docs/speech/) |
| Agent가 직접 조작하게 하고 싶음(Claude Code 등) | [LLM이 도구를 직접 호출(MCP)](https://war3ai.com/ko/docs/mcp/) | [UI와 입력](https://war3ai.com/ko/docs/ui-input/) |
| 다른 언어 사용(JS, C#, Go, Rust……) | [게이트웨이](https://war3ai.com/ko/docs/gateway/) | 더 낮은 계층: [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/) |
| 여러 사람의 AI끼리 대결시키고 싶음 | [공정 모드](https://war3ai.com/ko/docs/fair-mode/) | [아레나](https://war3ai.com/ko/arena/) |
| RPG / 커스텀 맵에서 나만의 플레이를 만들고 싶음 | [게임플레이 모드](https://war3ai.com/ko/docs/mods/) | [UI와 입력](https://war3ai.com/ko/docs/ui-input/), [캔버스](https://war3ai.com/ko/docs/canvas/), [JASS 채널](https://war3ai.com/ko/docs/jass/), [RPG 동료](https://war3ai.com/ko/docs/companion/) |
| 내 AI를 다른 사람과 공유하고 싶음 | [AI 스킴](https://war3ai.com/ko/docs/schemes/) | [Farsight 콘솔](https://war3ai.com/ko/docs/console/) |
## 저장소 구성
```text
start.bat 유일한 진입점: 처음부터 배포 + Farsight 열기. stop.bat은 모두 완전히 중지
sdk/python/ 인터페이스 계층. openwar3/가 외부용 파사드(Game + Bot)이며 여기서 시작합니다
brains/ 의사결정 계층
examples/ hello_bot(경제) → rush_bot(병력 생산) → macro_bot(운영) → micro_bot(컨트롤 + 크립 사냥); buddy(RPG 동료);
mod_hero_roguelike / mod_endless_defense(게임플레이 모드)
xwar3/ 레퍼런스 브레인: 전략 레이어(초 단위) + 반사 레이어(프로세스 4개) + 승률 모델
console/ Farsight 웹 콘솔(FastAPI + React)
gateway/ 게이트웨이(WebSocket / JSON) + JS 클라이언트 + 브라우저 데모 페이지
director/ 자동 카메라 연출, 머리 위 체력 바
speech/ 머리 위 채팅 말풍선 + 로컬 LLM
runtime/ 다중 인스턴스 오케스트레이션(게임마다 설정대로 재시작)
data/ order-ids.txt, 자신의 게임에서 데이터를 추출하는 도구
schemes/ 내 AI 스킴(mine/)과 다른 사람이 공유한 스킴(installed/), 저장소에는 포함되지 않음
tools/ play.py(명령 한 줄로 게임 시작), run_scheme.py(스킴 실행기), war3_mcp.py(MCP 서버), run_tests.py, 실게임 검증 스크립트
docs/ API 카탈로그 api.json(코드에서 생성), 프로토콜, 매뉴얼
```
런타임과 여러분의 코드 사이에는 버전이 붙은 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/) 하나만 있습니다. Python SDK가 가장 간편하지만, 다른 언어로 프로토콜을 따라 연결해도 됩니다.
## API의 "실측 상태"란
API 카탈로그의 모든 API에는 다음 세 상태 중 하나가 표시됩니다.
- **실게임 검증 완료**: 내부 경로(액션 번호, 파라미터 형태, 다시 읽어 온 효과)가 실제 대전에서 검증되었고, 검증 스크립트가 이를 지키고 있습니다.
- **실험**: 새로 추가된 API로, 테스트 인스턴스에서 동작을 확인했고 항목별 실게임 검증을 진행 중입니다. 사용할 수는 있지만 API 세부 사항은 바뀔 수 있습니다.
- **추정 / 실측 미완**: 내부 메커니즘은 엔진 자체의 방식(예: JASS의 동등 함수)을 그대로 따르지만, 아직 대전에서 항목별로 검증하지는 않았습니다. 사용하기 전에 회신부터 확인하세요.
> **참고**
>
> 현재는 **워크래프트 III 1.27**(프로즌 쓰론)만 지원합니다. 1.24 ~ 1.28은 같은 엔진 구조이며, 다중 버전 지원은 [로드맵](https://war3ai.com/ko/roadmap/)의 P4 단계에 있습니다. 1.29 이후 버전과 리포지드는 다른 엔진이므로 지원 약속 범위에 포함되지 않습니다.
---
# 빠른 시작
> start.bat을 더블클릭하면 필요한 것이 모두 자동으로 설치됩니다. Farsight에서 게임 디렉터리를 설정하고 게임을 시작해 예제 Bot이 지휘를 넘겨받는 모습을 확인합니다. 약 15분 걸립니다.
## 필요한 것
| | 요구 사항 | 설명 |
|---|---|---|
| 시스템 | Windows 10 / 11, 64비트 | 현재 Windows만 지원합니다 |
| 게임 | 워크래프트 III **1.27a**(프로즌 쓰론, `Game.dll` 1.27.0.52240) | 본인이 합법적으로 보유한 클라이언트. 디스크의 게임 파일은 전혀 수정하지 않습니다 |
**그 밖에는 미리 설치할 것이 없습니다.** `start.bat`이 내려받는 것은 딱 하나, Python 3.13(공식 포터블 패키지, 약 14MB)뿐입니다. 저장소의 `bin\env\`에 넣어 두며, 관리자 권한이 필요 없고 시스템 PATH도 바꾸지 않으며, 중국 내 네트워크에서는 자동으로 미러를 사용합니다. 컴퓨터에 이미 쓸 수 있는 Python이 있으면 그대로 사용합니다. PowerShell은 Windows에 내장된 것을 쓰고, Farsight의 웹 페이지는 저장소에 미리 빌드되어 제공되므로 Node.js가 필요 없습니다.
## 설치
1. **코드 받기**
```bash
git clone https://github.com/OPENXXAI/OpenWar3AI.git
```
또는 [압축 파일](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip)을 내려받아 풀어도 됩니다. 런타임(주입 DLL과 런처)은 저장소에 함께 들어 있으므로 따로 내려받지 않아도 됩니다.
2. **`start.bat` 더블클릭하기**
처음 실행하면 다음을 알아서 합니다.
- Python 3.13을 내려받습니다.
- Python 패키지를 설치하고, 런타임 파일을 확인한 뒤 설치합니다.
- AMAI를 내려받아 레퍼런스 브레인이 쓰는 전략 데이터를 생성합니다(AMAI는 자체 라이선스이며, 생성물은 git에 포함되지 않습니다. 실패해도 레퍼런스 브레인에만 영향이 있습니다).
- Farsight의 첫 화면인 "컨트롤 센터" `http://127.0.0.1:8866`을 엽니다.
단계마다 결과를 출력하고, 실패한 단계는 어떻게 보완하면 되는지 알려 줍니다. 그다음부터는 더블클릭할 때마다 1~2초 정도 확인만 하고 Farsight를 엽니다.
검은 창은 몇 초 뒤 저절로 닫힙니다. Farsight는 백그라운드에서 계속 실행되며, 브라우저를 닫아도 멈추지 않습니다.
3. **컨트롤 센터에서 게임 디렉터리 설정하기**
컨트롤 센터 맨 위에서 "자동 찾기"를 누르거나, "찾아보기…"로 워크래프트 III 디렉터리를 직접 고를 수 있습니다. Farsight는 게임 버전을 확인하고 **본인의 게임에서 데이터를 추출합니다**(유닛 테이블, 스킬, 아이템, 상성표…… Blizzard의 파일은 코드와 함께 배포하지 않습니다).
버전이 1.27a가 아니면 알려 줍니다. 맵과 다음 게임 설정은 모두 이 디렉터리를 기준으로 합니다(`<게임 디렉터리>\Maps` 아래 모든 폴더의 맵을 고를 수 있습니다). 나중에 디렉터리를 바꾸려면 "설정" 페이지에서 바꿉니다.
4. **게임을 시작하고 예제 Bot에게 맡기기**
가장 간단한 방법은 Farsight의 "인스턴스와 게임 시작" 페이지에서 인스턴스 번호 하나를 체크하고, AI 스킴을 고른 뒤 "테스트 시작"을 누르는 것입니다. 명령줄로도 할 수 있습니다:
```bash
python tools/play.py --bot brains/examples/hello_bot.py
```
이 명령은 게임 인스턴스를 띄우고, 런타임을 주입하고, 자동으로 게임을 시작한 다음 Bot을 실행합니다. **농부가 금을 캐러 가고 본진 건물이 농부를 생산하기 시작하면 성공입니다.**
명령의 `python`은 `openwar3.json`에 기록된 것을 씁니다. `start.bat`이 직접 설치한 Python은 `bin\env\python\python.exe`에 있습니다.
## start.bat과 stop.bat
```bash
start.bat # 배포 확인 + Farsight 열기
start.bat setup # 전체 확인: Python 패키지 재설치, AMAI 재시도
start.bat restart # Farsight 백엔드만 재시작(게임과 각 서비스는 영향 없음)
start.bat node # Node.js도 함께 설치(웹 페이지 미리보기 때만 필요, 평소에는 필요 없음)
start.bat 5 6 # 5, 6번 인스턴스의 테스트도 함께 시작(게임 + 레퍼런스 브레인)
stop.bat # 모두 완전히 중지. stop.bat --keep-llm은 로컬 모델을 VRAM에 남겨 둠
```
게이트웨이, 머리 위 말풍선, 로컬 LLM도 모두 Farsight "컨트롤 센터"에서 시작하고 중지하므로, 다른 스크립트를 찾을 필요가 없습니다. **완전히 중지하려면** `stop.bat`을 더블클릭하거나 컨트롤 센터 오른쪽 위의 "모두 중지"를 누르세요. 게임 인스턴스, AI, 게이트웨이, 말풍선, 이 시스템이 쓰는 로컬 모델, Farsight 백엔드가 차례로 모두 멈춥니다. MCP 서버는 Claude 같은 클라이언트가 관리하므로 중지되지 않습니다.
> **설정 파일**
>
> `openwar3.json`은 `start.bat`과 Farsight가 자동으로 작성하며, 이 PC의 경로만 저장하고 git에 포함되지 않습니다. 포트나 로컬 LLM의 주소와 모델 이름을 바꾸려면 `openwar3.example.json`을 참고해 그것과 다른 항목만 적으세요.
## play.py 인자
```bash
python tools/play.py --bot my_bot.py --inst 9 --race 2 --enemy-race 1 --difficulty 3 --speed 200
python tools/play.py --bot my_bot.py --inst 9 --attach # 게임이 이미 실행 중이면 Bot만 연결
python tools/play.py --bot my_bot.py --fair # 공정 모드: 시야 안의 것만 보임
```
| 인자 | 기본값 | 설명 |
|---|---|---|
| `--bot` | 필수 | Bot 파일 경로(파일 안에 `Bot`의 서브클래스가 하나 있어야 합니다) |
| `--inst` | `9` | 인스턴스 번호. 실행 중인 인스턴스와 번호가 겹치지 않게 하세요(Farsight의 "인스턴스와 게임 시작" 페이지에서 사용 중인 번호를 볼 수 있습니다) |
| `--race` | `1` | 아군 종족: 1 휴먼, 2 오크, 3 언데드, 4 나이트 엘프 |
| `--enemy-race` | `0` | 상대 종족 |
| `--difficulty` | `2` | 컴퓨터 상대 난이도: 2 쉬움, 3 보통, 4 Insane |
| `--speed` | `100` | 게임 배속(퍼센트, 200 = 2배속) |
| `--map` | 설정의 `default_map` | 맵 |
| `--attach` | | 게임을 띄우지 않고, 이미 실행 중인 인스턴스에 연결만 합니다 |
| `--hz` | `5` | 초당 `on_tick` 호출 횟수 |
| `--minutes` | `60` | 최대 실행 시간(분, 실제 시간) |
| `--fair` | | [공정 모드](https://war3ai.com/ko/docs/fair-mode/) |
| `--player` | | 몇 번 플레이어로서 지휘할지(AI 대 AI일 때 사용) |
> **주의**
>
> `--minimize`로 게임을 띄우지 마세요. **창이 최소화되어 있으면 게임 시뮬레이션이 멈춰 있어서**(시계가 흐르지 않음) Bot이 게임 시작을 끝없이 기다리게 됩니다.
`play.py`를 거치지 않고 SDK 명령줄로 이미 실행 중인 인스턴스에 바로 연결할 수도 있습니다:
```bash
python -m openwar3 run brains/examples/hello_bot.py --inst 5 # Bot 실행
python -m openwar3 status --inst 5 # 연결해서 스냅샷 / 고속 레인 상태 출력
python -m openwar3 catalog # API 목록 출력
```
## 실행에 성공했다면
- [첫 번째 Bot 작성하기](https://war3ai.com/ko/docs/first-bot/): 10줄짜리 최소 Bot에서 시작해 유닛 생산과 공격을 한 단계씩 추가합니다.
- [LLM에게 맡기기](https://war3ai.com/ko/docs/ai-bot/): 프롬프트 템플릿을 복사하고, 전략을 평범한 말로 설명하세요.
## 자체 점검
```bash
python tools/run_tests.py # SDK / 레퍼런스 브레인 / 반사 계층 / 콘솔 / 말풍선 / 예제, 스위트마다 하위 프로세스 하나
```
오프라인 테스트는 게임을 띄우지 않아도 됩니다. Farsight "컨트롤 센터"에도 환경 점검이 있어 각 부분이 제대로 설치됐는지 볼 수 있습니다.
---
# 첫 번째 Bot
> 10줄짜리 최소 Bot에서 시작해 농부 생산, 인구 확보, 병력 생산, 영웅, 공격을 추가하고 마지막으로 회신을 읽는 법을 익힙니다.
Bot은 `openwar3.Bot`을 상속한 클래스입니다. 필요한 훅만 오버라이드하면 되고, `g`(`Game`)가 "보기"와 "하기"를 맡습니다.
## 최소 Bot
```python title="my_bot.py"
from openwar3 import Bot
class MyBot(Bot):
def on_start(self, g): # 게임에 들어간 뒤 한 번 호출
g.message("내가 왔다")
def on_tick(self, g): # 초당 약 5번
for w in g.idle_workers():
g.gather(w, g.nearest(g.gold_mines(), w))
```
```bash
python tools/play.py --bot my_bot.py
```
놀고 있는 농부가 가장 가까운 금광으로 갑니다. 훅은 네 가지입니다:
| 훅 | 호출 시점 |
|---|---|
| `on_start(g)` | 게임에 들어간 뒤, 첫 틱 전에 한 번 |
| `on_tick(g)` | 매 틱(기본 초당 5번). 한 틱이 시간을 넘기면 자동으로 뒤로 밀리며, 밀린 틱이 쌓이지는 않습니다 |
| `on_event(g, ev)` | 매 틱의 `on_tick` 직전에, 이전 틱 이후의 이벤트를 하나씩 전달 |
| `on_end(g, reason)` | 이번 게임이 끝날 때(게임 프로세스 종료 / 아군 유닛 전멸 / 수동 중지) 한 번 |
> **팁**
>
> `on_tick`에서 예외가 발생해도 게임 전체가 중단되지는 않습니다. 러너가 스택 트레이스를 출력하고 다음 틱을 계속 진행하며, **20틱 연속으로 오류가 나야** 멈춥니다.
## 경제 추가: 농부 생산, 인구 확보
```python
from openwar3 import Bot
class Economy(Bot):
def on_tick(self, g):
res = g.resources() # 읽지 못하면 0이 아니라 None
halls = g.my_buildings({"htow", "hkee", "hcas"})
if res is None or not halls:
return
home = halls[0]
# 1. 놀고 있는 농부는 금 채집
for w in g.idle_workers():
mine = g.nearest(g.gold_mines(), w)
if mine:
g.gather(w, mine)
# 2. 농부 생산: 대기열에는 1개만(가득 채우면 돈이 대기열에 묶임)
if len(g.my_workers()) < 15 and not g.queue(home):
g.train(home, "hpea")
# 3. 인구가 거의 찼으면: 건물을 짓고 있지 않은 농부를 골라 본진 건물 근처에 농장 건설
if res["food_cap"] - res["food_used"] <= 6:
builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None)
if builder:
g.build_near(builder, "hhou", home.x, home.y)
```
눈여겨볼 작성법 세 가지:
- **`g.queue(home)`이 비어 있을 때만 훈련합니다.** 매 틱 훈련 명령을 내리면 7칸 대기열이 가득 차고 돈이 묶입니다(실측: 본진 건물에 농부 4명이 대기하면서 금 300이 대기열에 묶여 초반이 크게 늦어졌습니다).
- **좌표를 하드코딩하지 말고 `build_near`를 씁니다.** 가까운 곳부터 지을 수 있는 지점을 스스로 찾고, 여러 틱에 걸쳐 결과를 추적하며, 돈이 부족하면 아무것도 하지 않습니다. 하드코딩한 좌표는 숲 한가운데일 가능성이 큽니다.
- **건물을 짓고 있는 농부는 고르지 않습니다.** 휴먼 농장은 짓는 데 35초가 걸리는데, 도중에 일꾼을 빼면 기초 공사가 멈춥니다.
네 종족 모두에서 동작하는 완전한 버전은 `brains/examples/hello_bot.py`입니다. 금광당 5명, 금광이 차면 벌목, 멈춘 기초 공사 이어서 짓기까지 들어 있습니다.
## 병영, 영웅, 공격 추가
```python
from openwar3 import Bot
WAVE = 8
class Rush(Bot):
def on_start(self, g):
self.attacking = False
def on_tick(self, g):
halls = g.my_buildings({"htow", "hkee", "hcas"})
if not halls:
return
home = halls[0]
# 영웅: 제단은 있는데 영웅이 없으면 -> 먼저 부활, 부활이 안 되면 훈련(영웅은 유일해서, 죽은 뒤 다시 훈련하면 거부됨)
altars = g.my_buildings({"halt"})
if altars and not g.my_heroes():
if not g.revive(altars[0]):
g.train(altars[0], "Hpal")
for h in g.my_heroes():
info = g.hero_info(h)
if info and info["skill_points"]:
g.learn(h, "AHhb") # Holy Light
# 병영은 계속 보병 생산(대기열에는 1개만)
for b in g.my_buildings({"hbar"}):
if not g.queue(b):
g.train(b, "hfoo")
# 한 웨이브가 모이면 공격, 많이 잃으면 귀환
army = g.my_army()
if len(army) >= WAVE:
self.attacking = True
elif len(army) < WAVE // 2:
self.attacking = False
if self.attacking:
target = g.nearest([e for e in g.enemies() if g.is_building(e)], home)
if target:
idle = [u for u in army if not g.order_of(u)] # 놀고 있는 유닛에게만 명령
g.attack_move(idle, target.x, target.y)
```
완전한 버전은 `brains/examples/rush_bot.py`를 참고하세요(`hello_bot`을 상속하며, 병영 / 제단이 없으면 짓습니다).
## 회신 읽기
모든 명령은 회신 하나를 반환합니다. `if r:`은 "엔진이 수락했다"는 뜻이고, 수락되지 않았으면 `r.reason`에 이유가 적혀 있습니다:
```python
r = g.train(barracks, "hfoo")
if not r:
print(r.reason) # rejected(人口不够) = 인구 부족
print(r.verdict) # 3
```
자주 보는 원인 코드: `3` 인구 부족, `8` 금 부족, `9` 목재 부족, `32` 대기열 가득 참, `183` 선행 조건 부족, `221` 해당 항목 없음 / 건설 중 / 이미 있는 영웅, `1001` 대상이 보이지 않음. 전체 표는 [회신과 원인 코드](https://war3ai.com/ko/docs/reason-codes/)를 참고하세요.
> **수락 ≠ 성공**
>
> 회신은 "엔진이 이 명령을 받았다"는 것만 알려 줍니다. 숲 속 건설 지점도 엔진은 즉시 수락하고, 일꾼이 도착해서야 실패합니다. 스킬은 끊길 수도 있습니다. 결과는 스냅샷과 이벤트로 확인하세요. 건물은 `build_near`로 짓고(기초가 생겼는지 추적합니다), 스킬은 `g.cooldown()`이 쿨다운에 들어갔는지 보세요.
## 다음 단계
- [멘탈 모델](https://war3ai.com/ko/docs/concepts/): 스냅샷, 명령, 이벤트, 틱, 배치 — 왜 이렇게 설계했는가.
- [프로 운영 레시피](https://war3ai.com/ko/docs/cookbook/): 21가지 기술: 채집 포화, 인구 막힘 방지, 점사, 체력 낮은 유닛 빼기, 밤 사냥……
---
# LLM으로 Bot 만들기
> 프로그래밍을 몰라도 됩니다. 여러분은 어떻게 싸우고 싶은지 명확히 설명하고, 코드는 LLM이 씁니다. 프롬프트 템플릿을 복사해 전략을 설명하고, 실행해 본 뒤 다시 고쳐 달라고 하세요.
워크래프트는 잘 알지만 프로그래밍은 모르는 분께 적합하고, 시간을 아끼고 싶은 개발자에게도 좋습니다. 전체 과정은 하나의 대화입니다: **전략을 설명 → 모델이 코드 작성 → 한 게임 실행 → 본 현상을 모델에게 전달 → 모델이 수정**.
> **팁**
>
> 먼저 [빠른 시작](https://war3ai.com/ko/docs/quickstart/)을 따라 환경을 설치하고 `hello_bot`을 실행해 보세요(농부가 금을 캐러 가면 성공). 그래야 문제가 생겼을 때 환경 문제인지 Bot 문제인지 구분할 수 있습니다.
## 1. 모델에게 자료 주기
모델이 코드를 잘 쓰느냐는 대부분 올바른 자료를 읽었느냐에 달려 있습니다. 사용하는 도구에 맞춰 하나를 고르세요:
| 사용하는 도구 | 자료를 주는 방법 |
|---|---|
| **저장소를 읽을 수 있는 Coding Agent**(Claude Code, Cursor, Codex 등) | 저장소 디렉터리에서 실행하고, 먼저 `docs/BOT_HANDBOOK_ZH.md`, `docs/api.json`과 예제 하나(운영은 `brains/examples/macro_bot.py`, 전투는 `micro_bot.py`)를 읽게 하세요 |
| **인터넷에 접속할 수 있는 대화형 모델** | 먼저 [`https://war3ai.com/llms-full.txt`](https://war3ai.com/ko/llms-full.txt)를 읽게 하세요. 사이트의 모든 문서가 이 파일 하나에 들어 있습니다 |
| **인터넷에 접속할 수 없는 웹 대화** | 핸드북, [`api.json`](https://war3ai.com/ko/api.json), 예제 파일 하나를 프롬프트 뒤에 붙여 넣으세요 |
| **로컬 모델**(LM Studio, Ollama) | 위와 같습니다. 컨텍스트 창은 32K 토큰 이상을 권장합니다. 그보다 작으면 핸드북과 API 목록이 다 들어가지 않습니다 |
특정 프로 운영을 원하면 [프로 운영 레시피](https://war3ai.com/ko/docs/cookbook/)에서 해당 항목도 함께 붙여 넣으세요.
## 2. 이 프롬프트를 복사하기
마지막의 "원하는 전략"을 여러분의 말로 바꾸세요. 구체적일수록 좋습니다:
```text
워크래프트 III 1.27용 AI를 Python으로 작성해 줘. api.json에 나열된 Game 메서드만 사용하고,
존재하지 않는 메서드를 지어내지 마. 작성 방식은 rush_bot.py를 따라: openwar3.Bot을 상속하고 on_start(g)와 on_tick(g)를 구현해.
규칙:
- on_tick은 초당 약 5번 호출되니 빨라야 해(안에서 sleep하지 마).
- 읽지 못한 값은 0이 아니라 None이야. 먼저 확인하고 써.
- 명령은 회신(Receipt)을 반환해. `if r:`은 "엔진이 수락했다"는 뜻이고, 수락되지 않으면 `r.reason`에 이유가 적혀 있어
(인구 부족, 금 부족, 대상이 안 보임, 이미 있는 영웅……). 다음 틱에 다시 시도하거나 다른 방법을 써.
- 특정 적을 공격할 때는 g.attack(유닛, 적)을 써. 적은 반드시 시야 안에 있어야 하고, 안 보이면 거부돼.
- 영웅이 죽으면 g.revive(제단)으로 부활시켜야 해. 새로 훈련할 수는 없어.
- 건물은 g.build_near(일꾼, 건물 코드, x, y)로 지어: 지을 수 있는 지점을 스스로 찾고 결과를 추적하며, 돈이 부족하면 아무것도 하지 않아.
- "방금 무슨 일이 있었는지"(누가 죽었는지, 누가 피해를 입었는지, 영웅 레벨 업, 아이템 드롭)를 알고 싶으면 on_event(g, ev)를 구현해.
- 같은 유닛에게 매 틱 같은 명령을 반복하지 마(하던 일이 끊겨). "놀고 있는" 유닛에게만 명령해.
- 채집은 idle_workers()의 일꾼에게만 맡겨. 금광 하나에 일꾼은 최대 5명.
- 훈련 대기열에는 1개만 넣어(g.queue(건물)이 비었을 때 다음 것을 넣어). 인구가 막혔는지는 g.production(건물).blocked로 확인해.
- 한 틱에 명령을 많이 내릴 때는 with g.batch(): 로 감싸(게임 스레드를 한 번만 기다려).
- 누구를 칠지는 g.time_to_kill(내 유닛 무리, 적)으로 골라(상성과 방어력 반영). 어디로 갈지는 g.path_distance로 골라(갈 수 없으면 None).
- 공정 모드에서는 시야 안의 것만 보여. 전에 봤던 적은 g.last_seen()으로 확인해.
- 유닛은 4자 코드로 나타내(휴먼 농부 hpea, 보병 hfoo, 병영 hbar……). 스킬은 오더 문자열을 써(thunderbolt 폭풍 망치,
blizzard 눈보라, holybolt Holy Light……, 전체 목록은 data/order-ids.txt). 스킬 습득은 4자 코드를 써(AHtb, AHbz……).
원하는 전략:
<여기에 평범한 말로 쓰세요. 예:
"휴먼, 시작하면 농부 5명은 금, 1명은 벌목. 영웅은 대마법사 먼저. 병영 두 개에서 보병과 소총병.
12기가 모이면 영웅과 함께 상대 확장 기지를 공격. 영웅 체력이 30% 아래로 떨어지면 본진으로 후퇴.
사냥은 본진에서 가까운 캠프부터.">
```
### 전략을 명확하게 설명하는 법
모델은 모호한 요구를 가장 어려워합니다. "좀 더 공격적으로" 같은 말보다 다음 정보가 훨씬 유용합니다:
- **종족과 영웅**: 어떤 영웅을 먼저 뽑는지, 스킬 찍는 순서(예: 대마법사는 물의 정령, 눈보라, 물의 정령……).
- **빌드 순서**: 몇 번째 농부 때 병영을 짓는지, 언제 테크 업을 하는지, 병영은 몇 개인지.
- **병력 조합**: 보병 + 소총병? 몇 기가 되면 출발하는지?
- **진퇴 조건**: 몇 기가 모이면 공격하는지, 영웅 체력이 얼마 아래면 후퇴하는지, 많이 잃으면 본진으로 돌아가 다시 모으는지.
- **사냥**: 할지 말지, 언제 할지(해가 진 뒤?), 이길 수 있는 캠프만 칠지?
- **공정 여부**: 나중에 아레나에 올릴 생각이라면 "시야 안에 보이는 적만 사용"이라고 말하세요.
## 3. 실행하기
모델이 준 코드를 `brains/my_bot.py`로 저장하고:
```bash
python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2
```
결과를 빨리 보고 싶으면 `--speed 200`(2배속)을 붙이세요.
## 4. 고쳐 달라고 하기
- **오류가 났다면**: **오류 메시지 전체**를 그대로 모델에게 붙여 넣고 "고쳐 줘"라고 하세요.
- **잘 못 싸운다면**: 여러분이 추측한 원인이 아니라 **게임에서 본 것**을 설명하세요. 예: "영웅이 계속 본진에 서 있기만 한다", "유닛이 한 기씩 따로 들어가서 죽는다", "농부가 금광 하나에 몰려 있다".
- **새 전략을 추가하고 싶다면**: 한 번에 하나만 추가하고, 한 게임을 돌려 망가지지 않았는지 확인한 뒤 다음 것을 추가하세요.
> **참고**
>
> 직접 명령을 실행할 수 있는 Coding Agent라면 3, 4단계도 맡길 수 있습니다. 한 게임을 실행하고, 로그와 회신을 읽고, 코드를 고치고, 다시 실행합니다. Agent가 충분한 정보를 보게 하는 방법은 [Agent 자율 반복](https://war3ai.com/ko/docs/agent-loop/)을 참고하세요.
## 5. 자주 묻는 문제
| 현상 | 대개의 원인 |
|---|---|
| 아무것도 움직이지 않음 | 인스턴스 번호(`--inst`)가 틀렸거나, 게임이 아직 시작되지 않음 |
| 농부가 금을 캐지 않음 | 일하고 있는 농부에게 명령을 내림. `idle_workers()`에게만 맡기세요 |
| 건물이 계속 지어지지 않음 | 좌표를 하드코딩하지 말고 `build_near`를 쓰세요. 회신 `reason`이 돈 부족인지 확인하세요 |
| 영웅이 나오지 않음 | `train`의 회신을 보세요. 인구 부족인가요? 아니면 영웅이 죽었나요(`revive` 필요)? |
| 영웅이 스킬을 쓰지 않음 | 배우지 않았거나(`learn`) 마나가 없음. 사용한 뒤 `cooldown()`이 쿨다운에 들어갔는지 보세요 |
| 유닛이 틱마다 움찔거림 | 매 틱 명령을 다시 내리고 있음. 놀고 있는 유닛에게만 명령하세요 |
| 유닛이 안 나오고 돈만 쌓임 | 인구가 막힘: `g.production(병영).blocked`를 확인하세요 |
| 모델이 존재하지 않는 메서드를 씀 | 프롬프트에서 "api.json의 메서드만 사용"을 다시 강조하고, api.json 전체를 붙여 넣으세요 |
## 심화
- 모든 API와 각 API의 내부 메커니즘: [API 목록](https://war3ai.com/ko/api/)
- 레퍼런스 브레인(`brains/xwar3/strategy`)은 확장, 사냥, 공격까지 하는 완전한 AI입니다. 모델에게 그 사고방식을 읽게 할 수는 있지만, 더 저수준의 API를 쓰므로 그대로 베끼는 것은 권하지 않습니다.
- 나중에 [아레나](https://war3ai.com/ko/arena/)에 올리면 시야 안의 적만 볼 수 있습니다 — 지금부터 `--fair`로 스스로 제약을 걸어 두면 나중에 고칠 필요가 없습니다.
---
# Agent 자율 반복
> Coding Agent가 스스로 게임을 돌리고, 결과를 읽고, 코드를 고치고, 다시 돌리게 합니다. 이를 위해 무인 실행 명령 하나, 구조화된 게임 리포트 하나, 명확한 목표 하나가 필요합니다.
[LLM으로 Bot 만들기](https://war3ai.com/ko/docs/ai-bot/)에서는 "한 게임 실행 → 현상 관찰 → 모델에게 전달" 단계를 여러분이 직접 합니다. 명령을 실행할 수 있는 Coding Agent(Claude Code, Codex, Cursor의 Agent 모드 등)는 이 단계까지 넘겨받아 루프를 완성할 수 있습니다:
```text
코드 수정 ──► 한 게임 실행(무인) ──► 게임 리포트 읽기 ──► 가장 영향이 큰 한 곳 찾기 ──┐
▲ │
└───────────────────────────────────────────────────────────────────────────────────┘
```
이 루프가 실제로 수렴하려면 Agent에게 세 가지가 필요합니다.
## 1. 무인 실행 명령
```bash
python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
```
- `--minutes`는 게임이 반드시 끝나게 합니다(실제 시간 기준 분). Agent가 한 게임에 갇히지 않습니다.
- `--speed 200`은 2배속으로 시간을 아낍니다 — 단, Bot 안에서는 **게임 시계로 기다리고**(`g.clock()`), 실제 시간으로 `sleep`하지 마세요.
- `--fair`는 처음부터 아레나 규칙에 맞춰 작성하게 합니다. 시야 안의 것만 보입니다.
- 실행이 끝나면 터미널에 종료 원인이 출력됩니다. 예: `我方没有单位了`(아군 유닛 전멸), `到时间了`(시간 종료). Bot이 직접 `print`한 내용도 터미널에 나옵니다.
> **주의**
>
> 창이 최소화되어 있으면 게임 시뮬레이션이 멈춥니다. Agent가 기본 창 모드로 게임을 띄우게 하고, 여러분이 쓰고 있는 인스턴스와 번호(`--inst`)가 겹치지 않게 하세요.
## 2. 구조화된 게임 리포트
터미널 출력은 사람이 보기 위한 것입니다. Agent에게는 JSON이 적합합니다. 무슨 일이 있었는지, 무엇이 안 됐는지, 왜 안 됐는지. SDK가 재료는 이미 다 줍니다 — 회신에는 원인 코드가, 이벤트 스트림에는 생산 완료와 사상자가 있습니다. 이것들을 모으기만 하면 됩니다:
```python title="recorder.py"
import collections, json, time
from openwar3 import Bot
class Recorder(Bot):
"""Bot에 게임 리포트를 추가합니다. 이 클래스를 상속하고, 자신의 on_start / on_event에서 super()를 호출하세요."""
def on_start(self, g):
self.rejects = collections.Counter() # "train hfoo: rejected(人口不够)" -> 횟수
self.timeline = [] # [게임 초, 종류, 4자 코드]: 훈련 / 연구 / 건설 / 업그레이드 완료
self.lost = collections.Counter() # 아군이 잃은 것
self.killed = collections.Counter() # 아군이 처치한 것
def check(self, r, what):
"""명령을 감싸 거부 원인을 기록합니다: self.check(g.train(b, "hfoo"), "train hfoo")"""
if r is not None and not r:
self.rejects[f"{what}: {r.reason}"] += 1
return r
def on_event(self, g, ev):
me = g.me()
if ev.kind == "production.done" and ev.owner == me:
self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code])
elif ev.kind == "unit.died":
(self.lost if ev.owner == me else self.killed)[ev.type] += 1
def on_end(self, g, reason):
report = {"reason": reason, "timeline": self.timeline, "lost": self.lost,
"killed": self.killed, "rejects": self.rejects.most_common(10)}
try: # 게임이 이미 종료됐을 수 있음. 읽지 못하면 넘어감
report |= {"clock": g.clock(), "resources": g.resources(),
"army": len(g.my_army()), "workers": len(g.my_workers())}
except Exception:
pass
with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=1)
```
이 리포트로 답할 수 있는 질문:
| 신호 | 출처 | 알 수 있는 것 |
|---|---|---|
| 가장 많이 거부된 원인 | 회신 `reason` / `verdict` | 인구가 계속 막힘(3), 돈이 없는데 계속 명령함(8 / 9), 안개 속 대상을 공격함(1001), 영웅이 죽었는데 계속 훈련함(221) |
| 생산 타임라인 | `production.done` 이벤트(걸린 게임 초 포함) | 첫 영웅이 몇 초에 나왔는지, 몇 초에 테크 업을 했는지, 병영이 쉬지 않고 유닛을 뽑았는지. 프로 선수의 초반 빌드와 비교할 수 있습니다 |
| 양측 사상자 | `unit.died` 이벤트 | 병력을 계속 헛되이 잃고 있는지, 영웅이 몇 번 죽었는지, 사냥이 이득이었는지 |
| 종료 원인 | `on_end(g, reason)` | `我方没有单位了`(아군 유닛 전멸) = 패배, `到时间了`(시간 종료) = 아직 승부가 나지 않음 |
| 최종 병력과 자원 | `on_end` 시점에 스냅샷을 한 번 읽음 | 돈이 쌓여 있음 = 생산이 못 따라감, 일꾼이 너무 적음 = 경제가 성장하지 못함 |
> **참고**
>
> 프로그램으로 승패를 판정하는 것은 [아레나](https://war3ai.com/ko/arena/)의 기초 실험 중 하나로, 아직 로드맵에 있습니다. 지금은 "아군 유닛 전멸"로 패배를, "보이는 적 건물 전멸"로 근사적인 승리를 판정할 수 있습니다.
## 3. 명확한 목표와 몇 가지 제약
아래 내용을 목표에 맞게 고쳐서 Agent에게 주세요:
```text
목표: brains/my_bot.py가 Echo Isles에서 "쉬움" 난이도 컴퓨터를 안정적으로 이기게 한다(휴먼 대 무작위 종족).
매 라운드:
1. python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair 를 실행한다
2. 터미널 출력과 최신 run_*.json을 읽는다: 종료 원인, 생산 타임라인, 가장 많이 거부된 원인, 양측 사상자
3. 결과에 가장 큰 영향을 준 문제 "하나"를 찾아 그곳만 고친다. 코드 주석에 변경 이유와 근거 데이터를 적는다
4. 1단계로 돌아간다. 3게임 연속으로 개선이 없으면 멈추고, 리포트와 너의 판단을 나에게 알려 준다
제약:
- docs/api.json에 있는 메서드만 사용하고, API를 지어내지 않는다
- 같은 유닛에게 매 틱 같은 명령을 반복하지 않는다. 놀고 있는 유닛에게만 명령한다
- --fair를 유지한다(시야 안에 보이는 적만 사용)
- 코드를 고치기 전에 python tools/run_tests.py를 실행해 예제가 망가지지 않았는지 확인한다
```
## 루프를 더 빨리 수렴시키는 습관
- **한 번에 한 곳만 고치세요.** 세 곳을 동시에 고치면 이겼을 때 어느 것이 효과가 있었는지, 졌을 때 어느 것이 망쳤는지 알 수 없습니다.
- **비교할 때는 게임 수를 충분히 채우세요.** 같은 상황이라도 무작위성이 큽니다. 두 게임으로는 아주 큰 차이만 보입니다. "개선됐는가"는 적어도 몇 게임의 추세로 판단하세요.
- **전략을 조정하기 전에 "거부"부터 고치세요.** 회신에서 가장 많이 거부된 원인이 대개 Bot의 가장 큰 버그입니다.
- **판단을 주석에 남기세요.** 다음 라운드의 Agent(또는 다음 대화)가 주석을 보고 왜 이렇게 작성했는지 알 수 있어서, 고쳐 둔 곳을 되돌리지 않습니다.
- **오프라인 테스트로 안전망을 치세요.** 핵심 로직에는 게임을 띄우지 않아도 되는 단위 테스트를 작성하고(예제 Bot의 테스트는 `brains/examples/tests/`에 있습니다), Agent가 고칠 때마다 먼저 실행하게 하세요.
---
# LLM을 참모로
> "무엇을 모을지, 일꾼을 어디에 배치할지, 이번 1분은 싸울지 버틸지"를 LLM에게 맡기고, 규칙 계층은 실행과 거부만 담당합니다. 레퍼런스 브레인이 이미 이렇게 동작하며, 이 페이지에서는 그 패턴과 함정을 설명합니다.
Bot을 어느 정도 작성하다 보면 운영 규칙이 한 겹씩 덧붙여져 있다는 것을 알게 됩니다. 벌목 인원 규칙 하나, 금광당 5명 규칙 하나, 목재가 많으면 절반으로 줄이는 규칙 하나, 금이 모자라고 목재가 남으면 금 쪽에 더 보내는 규칙 하나…… 규칙 하나하나는 옳지만, 합쳐 놓으면 "금광에는 일꾼이 모자란데 농부가 전부 나무를 베고 있는" 상황, 즉 **어느 규칙도 책임지지 않는** 상황이 생깁니다.
"전체를 보고 우선순위를 정하는" 이런 판단은 애초에 `if / else`로 쓰기에 맞지 않지만, 바로 LLM이 잘하는 일입니다. 레퍼런스 브레인(`brains/xwar3/strategy/brain/coach.py`)은 아래와 같은 계층 구조를 씁니다.
## 계층 구조
```text
LLM(어드바이저) 20 게임 초마다 한 번, 비동기, 어떤 틱도 막지 않음
입력: 한 페이지짜리 게임 상황 스냅샷(자원, 인구, 농부 배치, 금광, 병종, 기술, 영웅, 적 정보, 최근 사건)
출력: 엄격한 JSON — 한 줄 진단 + 일꾼 배분 + 우선 생산 대상 + 이번 1분의 태세 + 하지 말 것
│
▼ 화이트리스트 + 상하한 클램핑 + 거부권
규칙 계층(Bot, 매 틱) 조언을 기존 기능의 "편향"으로 번역: 일꾼 배분, 건설 / 훈련 우선순위, 공격 태세
│
▼
실행 계층(SDK / 반사 계층) 명령, 회신 읽기, 마이크로 컨트롤
```
## 출력 계약
모델이 고정된 필드의 JSON만 출력하게 하고, 필드를 더하거나 빼지 못하게 합니다:
```json
{
"diagnosis": "한 문장: 게임 상황에서 가장 큰 문제. 반드시 입력 데이터에서 근거를 찾을 수 있어야 함",
"workers": { "gold": 10, "lumber": 6 },
"priority": ["hpea", "hhou", "hbar"],
"posture": "creep",
"avoid": ["목재가 부족할 때 Iron Plating 연구를 먼저 하지 말 것"]
}
```
| 필드 | 규칙 계층의 사용법 | 레퍼런스 브레인의 클램핑 |
|---|---|---|
| `workers` | 금 채집, 벌목의 목표 인원 | 금 2 ~ 25, 벌목 1 ~ 20. 둘의 합은 전체 농부 수를 넘지 않음 |
| `priority` | 훈련 / 건설 / 연구의 우선순위 | 최대 4개. "선택 가능 코드" 표에 있는 4자 코드만 받음 |
| `posture` | 이번 1분의 태세 | `attack` `defend` `creep` `expand` `recover` `hold` 중 하나만 가능 |
| `avoid` | 이번 1분 동안 하지 말 것 | 최대 2개 |
| `diagnosis` | 로그와 콘솔 표시에만 사용 | — |
종족마다 프롬프트를 따로 두고, 그 종족에만 있는 선택지만 적습니다(휴먼의 협동 건설과 민병대, 오크의 굴, 언데드의 저주받은 금광(Haunted Gold Mine), 나이트 엘프의 휘감은 금광(Entangled Gold Mine)……). 공통 규칙은 공통 부분에 두고, 네 번 복사하지 마세요.
## 네 가지 강한 제약
네 가지 모두 레퍼런스 브레인이 실제로 대가를 치르고 얻은 교훈입니다:
1. **어드바이저는 절대 유닛에게 직접 명령하지 않습니다.** 어드바이저는 150 ms 단위의 현장을 볼 수 없고, 환각도 일으킵니다. 목표와 우선순위만 바꾸고, 누가 어디로 가서 누구를 공격할지는 여전히 규칙 계층과 반사 계층이 정합니다 — 지휘권의 주인은 하나여야 합니다.
2. **비동기.** 어드바이저 호출 한 번에 약 1초가 걸리며, 백그라운드 스레드에서 돌고 최신 결과가 적용됩니다. **어떤 틱도 절대 막지 않습니다.** 모델이 떠 있지 않거나, 시간이 초과되거나, 엉뚱한 답을 하면 이 계층이 없는 것으로 보고 순수 규칙으로 돌아갑니다. 너무 오래된 조언(3 간격 초과)도 쓰지 않습니다.
3. **화이트리스트 + 클램핑.** 모든 필드는 기존 기능에 대응해야 하고, 수치는 합리적인 범위로 클램핑합니다. 알 수 없는 내용은 조용히 무시하지 말고 **카운트한 뒤 버립니다**.
4. **전 과정 카운트.** 몇 번 물었는지, 몇 번 성공했는지, 몇 번 시간 초과됐는지, 몇 번 클램핑됐는지, 필드별로 몇 번 채택됐는지를 마지막으로 모델에 보낸 입력과 함께 발행합니다. 그렇지 않으면 "이 계층이 정말 쓸모가 있는가"는 답할 수 없는 질문이 됩니다.
> **안전하게 성능을 낮출 수 있는 것일수록 조용히 낮아지기 쉽다**
>
> 어드바이저는 "실패 = 이 계층이 없는 것"으로 설계되어 있으므로, 모델 서비스가 떠 있지 않으면 Bot은 순수 규칙과 똑같이 동작하고 밖에서는 전혀 티가 나지 않습니다. 레퍼런스 브레인은 인스턴스 6개의 어드바이저가 하루 종일 모두 연결되지 않았는데도 아무도 눈치채지 못한 적이 있습니다. "마지막 성공 시각"과 "마지막 실패 원인"은 반드시 발행하세요 — [Farsight 콘솔](https://war3ai.com/ko/docs/console/)의 "운영 어드바이저" 페이지가 바로 그 역할을 합니다.
## 여러분의 Bot에 구현하기
아래는 최소한의 뼈대입니다. OpenAI 호환 API라면 무엇이든(LM Studio, Ollama, 클라우드 API 모두) 쓸 수 있고, 표준 라이브러리만 사용합니다:
```python title="coached_bot.py"
import collections, json, threading, urllib.request
from openwar3 import Bot
BASE = "http://127.0.0.1:1234/v1" # LM Studio / Ollama / 모든 OpenAI 호환 서비스
MODEL = "your-model"
POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"}
SYSTEM = """너는 워크래프트 III의 운영 코치다. 운영과 전략만 담당하고, 마이크로 컨트롤은 다루지 않는다.
JSON만 출력하고, 필드는 고정이다: {"diagnosis": 한 문장, "workers": {"gold": 정수, "lumber": 정수},
"priority": [4자 코드, 최대 4개, allowed에 있는 것만], "posture": 여섯 중 하나, "avoid": [최대 2개]}
주어진 게임 데이터만 근거로 말하고, 데이터에 없는 것은 지어내지 마라."""
def ask(state: dict) -> dict:
body = {"model": MODEL, "temperature": 0.3, "max_tokens": 260,
"messages": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": json.dumps(state, ensure_ascii=False)}]}
req = urllib.request.Request(f"{BASE}/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=8) as r:
text = json.load(r)["choices"][0]["message"]["content"]
return json.loads(text[text.index("{"): text.rindex("}") + 1])
class CoachedBot(Bot):
EVERY = 20.0 # 게임 초: 운영 결정의 시간 단위는 분이므로 매 틱 물을 필요 없음
allowed = {"hpea", "hfoo", "hrif", "hkni", "hhou", "hbar", "hbla", "Rhme", "Rhar"}
def on_start(self, g):
self.plan, self.plan_at, self.asked_at, self.busy = {}, -1e9, -1e9, False
self.stats = collections.Counter()
def summary(self, g) -> dict: # 스냅샷은 메인 스레드에서 읽어 두고, 백그라운드 스레드는 g를 건드리지 않음
res = g.resources() or {}
return {"clock": round(g.clock() or 0), "gold": res.get("gold"), "lumber": res.get("lumber"),
"food": [res.get("food_used"), res.get("food_cap")],
"workers": len(g.my_workers()), "idle_workers": len(g.idle_workers()),
"army": collections.Counter(u.type for u in g.my_army()),
"enemy_seen": collections.Counter(u.type for u, _t, _age in g.last_seen(max_age=90)),
"night": g.is_night(), "allowed": sorted(self.allowed)}
def consult(self, state, now):
try:
p = ask(state)
self.stats["ok"] += 1
posture = p.get("posture")
if posture not in POSTURES:
self.stats["bad_posture"] += 1 # 카운트한 뒤 버림, 조용히 무시하지 않음
posture = "hold"
self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))), # 클램핑
"lumber": min(20, max(1, int(p["workers"]["lumber"]))),
"priority": [c for c in p.get("priority", []) if c in self.allowed][:4],
"posture": posture}
self.plan_at = now
except Exception as e: # 시간 초과 / 엉뚱한 답: 이 계층이 없는 것으로 처리
self.stats[f"error:{type(e).__name__}"] += 1
finally:
self.busy = False
def on_tick(self, g):
now = g.clock() or 0.0
if not self.busy and now - self.asked_at >= self.EVERY:
self.busy, self.asked_at = True, now
threading.Thread(target=self.consult, args=(self.summary(g), now), daemon=True).start()
plan = self.plan if now - self.plan_at <= 3 * self.EVERY else {} # 너무 오래된 조언은 쓰지 않음
# ↓ 규칙 계층: plan이 비어 있으면 기본 규칙대로. plan이 있으면 배분, 우선순위, 태세만 조정하고 실제 명령은 여전히 규칙이 결정
...
```
## 모델 고르기
| 상황 | 권장 사항 |
|---|---|
| 로컬, 빨라야 함 | MoE 모델(호출마다 일부 파라미터만 활성화)은 같은 크기의 dense 모델보다 훨씬 빠릅니다. 레퍼런스 브레인은 Qwen3.6-35B-A3B(LM Studio, Q4)를 쓰며, 중앙값 **1.09초**, 최대 1.45초, 출력 5/5가 바로 `json.loads`됩니다 |
| 로컬, "생각하는" 모델 | **생각 구간을 반드시 꺼야 합니다.** 그렇지 않으면 토큰을 전부 생각에 써 버려 JSON이 하나도 나오지 않습니다. LM Studio는 `/no_think`를 무시합니다. 레퍼런스 브레인은 `/v1/completions`로 바꿔 ChatML을 직접 조립하고, 빈 ``와 `{` 하나를 미리 채워 넣습니다 |
| 클라우드 모델 | 지연은 보통 더 크지만, 이 계층 구조는 원래 비동기입니다. 운영 결정은 분 단위이므로 몇 초의 지연은 괜찮습니다 |
> **참고**
>
> 같은 모델로 유닛에게 목소리를 입힐 수도 있습니다: [말풍선과 로컬 모델](https://war3ai.com/ko/docs/speech/)을 참고하세요. 모델이 (참모가 아니라) 매 틱 직접 명령하게 하고 싶다면 [아레나](https://war3ai.com/ko/arena/)의 JSON 게이트웨이를 기다리세요.
---
# LLM이 도구를 직접 호출(MCP)
> tools/war3_mcp.py는 MCP 서버입니다. Claude Code, Claude Desktop 또는 MCP를 지원하는 어떤 클라이언트든 이 서버를 연결하면, LLM이 코드를 먼저 쓰지 않고도 게임 상황을 보고, 명령을 내리고, 화면에서 플레이어에게 말하고, 카드를 띄워 플레이어에게 묻고, 스크린숏으로 화면을 볼 수 있습니다.
`tools/war3_mcp.py`는 **MCP 서버**(stdio)입니다. Claude Code, Claude Desktop, 로컬 모델의 Agent 프레임워크 — MCP를 지원하는 어떤 클라이언트든 이 서버를 연결하면, LLM이 코드를 먼저 쓰지 않고도 **직접** 게임 상황을 보고, 명령을 내리고, 게임 화면에서 플레이어에게 말하고, 플레이어에게 묻고, 스크린숏으로 화면을 볼 수 있습니다.
Bot 작성, 참모, 유닛 대사에 이은 또 하나의 연결 방식입니다. **LLM이 스스로 도구의 사용자가 됩니다.**
## 연결하기
```bash
claude mcp add war3 -- python <저장소>\tools\war3_mcp.py --inst 9 # Claude Code. <저장소>는 자신의 openwar3 폴더로 바꾸세요
```
다른 클라이언트는 이 형식으로 설정을 씁니다.
```json
{"mcpServers": {"war3": {"command": "python", "args": ["<저장소>\\tools\\war3_mcp.py", "--inst", "9"]}}}
```
처음 도구를 호출할 때 게임에 연결하므로, 게임은 나중에 켜도 됩니다. 게임을 껐다가 다시 켜면 다음 호출 때 자동으로 다시 연결합니다. `--role`을 붙여 LLM이 할 수 있는 일을 제한합니다.
| 역할 | 쓸 수 있는 것 |
|---|---|
| `dev`(기본값) | `war3_jass`를 포함한 모든 도구 |
| `player --player N` | N번 플레이어의 유닛만 지휘하고, 그 시야만 볼 수 있음(공정 모드). JASS 없음 |
| `observer` | 읽기 전용, 화면에 그릴 수 없고 유닛이 말하게 할 수도 없음. 내린 명령은 런타임이 바로 거부 |
`player` 역할의 제한은 [게이트웨이](https://war3ai.com/ko/docs/gateway/)와 같습니다. 게임 종료, 속도 변경, 일시 정지를 쓸 수 없고, 다른 플레이어의 패를 볼 수 있는 API도 쓸 수 없으며, 플레이어 번호를 받는 조회는 자기 자신만 조회할 수 있습니다.
몇 가지 상한: 도구 결과 하나는 최대 20만 자이며, 넘으면 잘라 내고 범위를 좁히는 방법을 알려 줍니다. `war3_ask_player`는 최대 120초까지 기다립니다. 스크린숏의 `scale`은 0.1에서 1 사이입니다.
## 도구
| 도구 | 하는 일 |
|---|---|
| `war3_overview` | 게임 상황 한 페이지: 시간, 자원, 식량, 아군 유닛 종류별 수, 영웅(체력, 마나, 레벨, 쿨다운), 보이는 적 유닛 종류, 생산. **이것부터 호출하세요** |
| `war3_units` | 유닛 목록(`owner`는 me / enemy / creep / all, `types`로 필터링). `addr`로 명령을 내림 |
| `war3_events` | 지난 호출 이후 일어난 일: 사망, 레벨 업, 시전, 생산 완료, 채팅, 플레이어가 버튼을 클릭함……(기본적으로 너무 자주 발생하는 몇 종류는 제외) |
| `war3_call` | 아무 공개 API나 호출(`move`, `attack_move`, `train`, `build`, `cast`, `learn`, `ui.button`, `canvas.text`……). 유닛은 `{"unit": addr}`로 씀 |
| `war3_api` | API 조회: 키워드로 이름과 설명을 검색 |
| `war3_toast` / `war3_say` | 화면 위쪽에 한 줄 표시 / 유닛 머리 위에 한 마디 |
| `war3_ask_player` | 화면 가운데에 선택 카드 몇 장을 띄우고, 플레이어가 클릭하기를 기다렸다가 무엇을 골랐는지 반환(게임 일시 정지 가능) |
| `war3_screenshot` | 게임 화면 스크린숏(PNG. 창이 가려져 있어도 찍을 수 있고, 포커스를 빼앗지 않음) |
| `war3_jass` | JASS 코드 실행(dev 전용. 월드 변경은 싱글플레이 게임에서만) |
이런 것을 할 수 있습니다.
- **같이 플레이 / 코치**: `war3_overview`로 게임 상황을 보고, `war3_toast`로 화면에 조언을 띄움
- **플레이하면서 플레이어에게 묻기**: `war3_ask_player`로 카드 세 장을 띄우고, 플레이어가 고른 대로 진행
- **해설**: `war3_events`로 무슨 일이 있었는지 읽고, `war3_say`로 유닛이 직접 말하게 함
- **부대 하나를 직접 지휘**: `player` 역할 + `war3_call`. 자기 유닛만 움직일 수 있음
- **화면을 보며 UI 조정**: `war3_screenshot`으로 한 장 찍어, 직접 그린 버튼이 제자리에 있는지 확인
## 대화는 대략 이렇게 흘러갑니다
```text
나: 지금 게임 상황을 보고, 화면에서 나한테 물어봐 줘. 다음은 멀티, 병력 생산, 본진 업그레이드 중 뭘로 할까?
→ war3_overview {}
← 게임 상황 한 페이지: 게임 시간, 금 500, 식량 10/12, 아군 htow 1 · hpea 5 · Hpal 1, 보이는 적 없음, 생산 중인 것 없음
→ war3_ask_player {"question": "다음은?", "options": ["병력 생산", "멀티", "본진 업그레이드"], "pause": true}
← {"picked": 1, "option": "멀티"}
모델: 멀티를 고르셨네요. 먼저 war3_units로 놀고 있는 일꾼을 하나 찾고, 가장 가까운 금광이 어디인지 볼게요……
```
## 실측
2026-09-25:
- 자체 MCP 클라이언트로 실제 게임에 연결, 7/7: 핸드셰이크 → 도구 나열(10개) → `war3_overview`(`htow` 1, `hpea` 5) → `war3_units` → `war3_toast` → `war3_screenshot`(PNG 약 20만 바이트) → `war3_ask_player`(카드 세 장, 두 번째 카드 클릭을 시뮬레이션 → `{"picked": 1, "option": "멀티"}`).
- Claude Code 2.1에 실제로 연결: Claude Code가 직접 서버를 띄우고 핸드셰이크했으며, 상태는 `connected`, 도구 10개가 모두 `mcp__war3__*`로 도구 목록에 나타났습니다.
## 구현
- 줄바꿈으로 구분한 JSON-RPC 2.0(`initialize` / `tools/list` / `tools/call` / `ping`), 프로토콜 버전 2025-06-18, 2025-03-26과 2024-11-05도 호환.
- 도구 오류는 MCP 규칙에 따라 결과 안에 담으며(`isError: true`), 연결을 끊지 않습니다.
- [게이트웨이](https://war3ai.com/ko/docs/gateway/)와 같은 역할 화이트리스트, 유닛 인자 형식, "게임 상황 한 페이지"를 공유합니다.
- 로그는 stderr로 보내고, stdout에는 프로토콜만 씁니다.
---
# 멘탈 모델
> 스냅샷, 명령, 회신, 이벤트, 틱, 배치. 이 여섯 가지 개념을 이해하면 API가 왜 이런 모양인지, 어떻게 써야 빠른지 알 수 있습니다.
## 스냅샷: 읽기, 대기 없음
런타임은 **50 ms**마다 게임 스레드에서 월드 전체를 수집해 공유 메모리에 씁니다. `g.snapshot()`으로 얻는 것은 **완전하고 일관된** 월드입니다:
- 16개 플레이어 슬롯: 금, 목재, 인구, 인구 상한, 누적 채집량, 종족
- 최대 1024개 유닛: 종류, 소유자, 좌표, 체력 / 마나(최대치 포함), 현재 오더와 오더 대상, **실제로 공격 중인 대상**(작업 대상), 영웅 레벨 / 경험치 / 스킬 포인트, 각 플레이어에 대한 가시성
- 최대 256개 유닛 상세 정보: 스킬 12개(레벨, 남은 쿨다운), 버프 8개, 인벤토리 6칸
- 바닥의 아이템, 나무(2초마다 갱신), 생산 테이블(훈련 / 연구 / 건설 / 업그레이드 진행도), 게임 시계, 게임 내 시각
한 번 읽는 데 약 **0.4 ms**(Python 파싱)가 걸리며, 게임 스레드를 기다리지 않습니다. 그러니 **마음껏 읽으세요**. `g.units()`, `g.my_army()`, `g.cooldown()`, `g.inventory()` 같은 API는 모두 같은 스냅샷에서 값을 꺼내므로, 한 틱에 몇 번을 호출해도 비용이 크지 않습니다.
> **팁**
>
> 발행 주기는 `g.set_publish_period(ms)`로 16 ~ 1000밀리초 사이에서 조정할 수 있습니다. 한 번 수집하는 데 게임 스레드에서 약 0.5 ~ 0.9 ms가 걸리므로 33 ms도 문제없습니다. 기기 전체가 하나의 값을 공유하며, 마지막에 쓴 값이 적용됩니다.
## 명령: 쓰기, 약 한 프레임
`g.move / attack / gather / build / train / cast …`는 게임 스레드가 실행합니다. 런타임은 게임 스레드의 **이벤트 디스패치** 안에서 클라이언트가 제출한 명령을 모아서 실행하므로, 명령 하나는 약 **한 프레임**을 기다립니다(이벤트가 몰려 있는 구간에 걸리면 약 0.1 ms, 아니면 다음 디스패치까지).
- 명령에는 **유닛 하나 또는 목록**을 넘길 수 있으며, 목록의 유닛은 같은 프레임에 함께 명령을 받습니다.
- `queue='after'`를 붙이면 Shift 예약입니다. 지금 하는 일을 끝낸 뒤에 실행합니다.
- 명령에 넘기는 유닛은 스냅샷에서 얻은 객체를 그대로 쓰면 됩니다. SDK는 **핸들 쌍**으로 유닛을 식별합니다(주소는 새 유닛이 재사용할 수 있지만 핸들은 그렇지 않습니다).
## 회신: 모든 명령에 있음
```python
r = g.build(worker, "hbar", x, y)
if r: # 엔진이 수락함
...
else:
r.reason # 'rejected(金不够)' = 금 부족
r.verdict # 8
r.exec_us # 이 명령이 게임 스레드에서 실행된 마이크로초
```
회신은 **같은 프레임** 안에서 읽어 옵니다. 명령 전후의 유닛 오더, 엔진 함수의 반환값, 실행 가능성 검사의 원인 코드가 담겨 있습니다. 회신은 "엔진이 이 명령을 받았는가, 왜 받지 않았는가"에 답하지만, "결국 해냈는가"에는 **답하지 않습니다** — 해냈는지는 스냅샷과 이벤트로 확인합니다.
모든 상태 코드와 원인 코드는 [회신과 원인 코드](https://war3ai.com/ko/docs/reason-codes/)를 참고하세요.
## 이벤트: 무슨 일이 일어났는가
`on_event(g, ev)`는 매 틱의 `on_tick` 직전에, 이전 틱 이후 발생한 이벤트를 하나씩 전달합니다:
| 이벤트 | 의미 |
|---|---|
| `unit.appeared` / `unit.died` / `unit.removed` | 유닛 등장, 사망, 소멸(금광에 들어감, 변환됨, 시체가 썩음도 소멸이며 사망과는 다릅니다) |
| `unit.damaged` / `order.changed` / `owner.changed` | 체력 감소, 오더 변경, 소유자 변경 |
| `hero.levelup` | 영웅 레벨 업 |
| `item.appeared` / `item.removed` | 바닥의 아이템 등장, 누군가 주워 가거나 사용됨 |
| `damage` | 엔진 수준: **타격마다** 발생하는 피해. 공격한 유닛, 공격 타입, 피해 타입, 실제로 깎인 체력, 방어력 적용 전 피해 |
| `killed` | 엔진 수준: 이 타격으로 대상이 죽음. 처치한 유닛 포함 |
| `production.done` | 훈련 / 연구 / 건설 / 업그레이드 완료. 4자 코드와 걸린 게임 초 포함. 상대의 것도 옵니다 |
| `spell.cast` | 유닛이 스킬을 시전함: 스킬 4자 코드, 레벨, 쿨다운(초), 시전 지점 |
| `message` | 화면 메시지 영역에 한 줄이 표시됨: 게임 안내("농장이 더 필요합니다"), 채팅(`.chat`에 발신자와 내용), 시스템 메시지 |
| `selection.changed` / `player.left` | 로컬 플레이어의 선택이 바뀜 / 플레이어가 나가거나 패배 판정으로 제거됨 |
| `game.started` / `game.ended` | 새 게임 시작 / 게임에서 나감 |
캔버스 버튼 클릭, 단축키, 지면 클릭 같은 입력 이벤트는 [UI와 입력](https://war3ai.com/ko/docs/ui-input/)을 참고하세요.
> **주의**
>
> 이벤트 스트림은 **전역**입니다. 상대의 생산 완료, 크립의 사망도 모두 들어 있습니다. `ev.owner`나 유닛 핸들로 필터링하세요.
## 틱: Bot의 리듬
`on_tick`은 기본적으로 초당 5번 호출됩니다(실제 시간 기준). 한 틱의 소요 시간은 사실상 여러분의 계산 시간입니다. 스냅샷은 대기가 없고, 명령은 약 한 프레임입니다. 한 틱이 주기를 넘기면 자동으로 뒤로 밀리며, 밀린 틱이 쌓이지는 않습니다.
- **2배속에서는 실제 시간으로 기다리지 마세요.** 3 게임 초를 기다리려면 `g.clock()`이 3만큼 늘었는지 보세요. `sleep(1.5)`를 쓰면 안 됩니다.
- **`on_tick` 안에서 `sleep`하지 마세요.** "잠시 뒤에 하기"가 필요하면 현재 게임 시각을 기록해 두고 다음 틱에 다시 판단하세요.
## 배치: 명령 수십 개도 한 번만 대기
한 틱에 명령을 많이 내려야 할 때는 `with g.batch():`로 감쌉니다:
```python
with g.batch():
g.attack(melee, target_a)
g.attack(ranged, target_b)
g.move(wounded, home.x, home.y)
g.cast(hero, "thunderclap")
# 블록이 끝날 때 배치 전체를 제출: 같은 프레임에 실행, 게임 스레드를 한 번만 기다림
```
- 블록 안의 명령은 `Pending`을 반환하며, 블록이 끝난 뒤 회신이 됩니다. 블록이 끝나기 전에 읽으면 예외가 발생합니다.
- 블록 안에서 예외가 발생하면 **배치 전체가 취소됩니다**(반쯤 실행된 명령은 아예 보내지 않는 것보다 위험합니다).
- 실측 이동 명령 8개: 하나씩 보내면 68 ~ 99 ms, 배치로 보내면 **6.5 ~ 10 ms**.
같은 방식을 쿼리에도 쓸 수 있습니다. `g.can_do_many([(u, code), ...])`, `g.tech_many([...])`로 여러 개를 한 번에 묻습니다.
## 방금 쓴 것 읽기
같은 틱 안에서는 방금 내린 명령이 스냅샷에 아직 보이지 않습니다(다음 발행 때 반영됩니다). 그래서 두 로직이 같은 일꾼을 두고 다툴 수 있습니다. 한쪽은 방금 농장을 지으라고 보냈는데, 다른 쪽은 스냅샷을 보고 아직 놀고 있다고 판단하는 경우입니다.
`g.order_of(u)`가 이 문제를 해결합니다. 스냅샷이 따라잡기 전까지는 회신에 담긴 새 오더를 기준으로 합니다. **"놀고 있는가"는 `u.order`가 아니라 `g.order_of(u)`로 판단하세요.** `g.idle_workers()`는 "이번 틱에 방금 일을 맡은 일꾼"을 이미 제외합니다.
## 지연 단계
| 단계 | 채널 | 지연 | 용도 |
|---|---|---|---|
| 0 | 푸시 스냅샷 + 이벤트 스트림 | 한 번 읽기 약 0.4 ms, 데이터는 50 ms마다 갱신 | 모든 "보기" API |
| 1 | 고속 레인 | 약 1프레임, 6개 프로세스 동시 실행 시 중앙값 0.06 ms | 모든 명령과 쿼리(SDK 기본값) |
| 2 | 제어 채널 | 20 ~ 40 ms | 예비 경로, 일부 UI 성격의 작업(배속, 말풍선, 메시지) |
| 3 | [게이트웨이](https://war3ai.com/ko/docs/gateway/)(WebSocket / JSON) | 1단계 + 약 1 ms | 모든 언어, 브라우저, LLM, 다른 컴퓨터의 프로그램 |
[API 목록](https://war3ai.com/ko/api/)에는 API마다 어느 단계를 쓰는지 표시되어 있습니다.
---
# 15가지 규칙
> 모두 실제 대전에서 부딪히며 얻은 규칙입니다. Bot을 작성할 때 한 번씩 대조해 보면 디버깅 시간을 대부분 아낄 수 있습니다.
> **팁**
>
> 이 페이지를 [`api.json`](https://war3ai.com/ko/api.json)과 함께 LLM에 건네면, 훨씬 덜 헤매는 Bot을 작성합니다.
## 상태 읽기
### 1. 읽을 수 없으면 0이 아니라 `None`
`resources()`, `time_of_day()`, `production()`, `cooldown()`은 모두 `None`을 반환할 수 있습니다(로딩 중, 유닛에 세부 정보가 없음, 건물이 생산 중이 아님……). 먼저 확인하고 사용하세요.
```python
res = g.resources()
if res is None:
return
```
### 2. 유닛은 주소가 아니라 핸들로 식별하기
주소는 새 유닛이 재사용합니다. 예전 주소가 새로 태어난 유닛을 가리킬 수 있습니다. 여러 틱에 걸쳐 특정 유닛을 기억하려면 `u.handle`을 저장하고 `g.unit(handle)`로 다시 찾으세요.
### 3. 이벤트 스트림은 전역입니다
`production.done`, `unit.died`에는 상대와 크립의 이벤트도 들어 있습니다. `ev.owner`(또는 건물 핸들)로 필터링하세요.
```python
if ev.kind == "production.done" and ev.owner == g.me():
...
```
### 4. 금광에 들어간 일꾼은 스냅샷에 없습니다
일꾼은 금광에 들어가는 순간 스냅샷에서 사라집니다(`unit.removed`이며, 죽은 것이 아님). 금광마다 인원을 집계하려면 **직접 기록**하고, 스냅샷에 없다고 장부에서 지우지 마세요. 그러지 않으면 이미 가득 찬 금광에 일꾼을 더 보내게 됩니다.
## 명령 내리기
### 5. 회신 "수락" ≠ 완료
숲속 건설 지점도 엔진은 그 자리에서 수락하고, 일꾼이 도착해서야 실패합니다. 스킬은 끊길 수 있습니다. 효과는 스냅샷과 이벤트로 확인하세요. 건물은 `build_near`로 짓고(건설 부지가 나타나는지 추적함), 스킬은 `g.cooldown()`이 쿨다운에 들어갔는지 봅니다.
### 6. 보이지 않는 대상은 공격할 수 없습니다
전장의 안개 속 적에게 대상 지정 명령을 내리면 사유 코드 **1001**로 거부됩니다. 안개 속 적을 쫓으려면 마지막으로 보였던 위치로 `attack_move`하세요.
### 7. 쉬고 있는 유닛에게만 명령하기
매 틱 같은 유닛에게 같은 명령을 다시 내리면 동작이 끊깁니다. 병력은 제자리에서 버벅이고, 일꾼의 채집 주기는 처음부터 다시 시작됩니다. "쉬고 있는지"는 `g.order_of(u)`(이번 틱에 방금 내린 명령 포함)로 판단하고, 스냅샷의 `u.order`(스냅샷이 아직 따라잡지 못함)는 쓰지 마세요.
### 8. Shift는 "현재 명령 바로 뒤에 끼워 넣기"만 됩니다
엔진에는 "맨 끝에 추가"가 없습니다. `queue='after'`로 B, C를 연달아 보내면 A, C, B가 됩니다. 여러 지점을 순서대로 돌려면 `g.path(units, 지점 목록)`을, 일꾼 한 명이 여러 채를 연속으로 지으려면 `g.build_queue(worker, 계획)`을 쓰세요. 둘 다 역순으로 끼워 넣어 순서를 알아서 맞춰 줍니다.
### 9. 한 틱의 명령은 한 배치로
명령 수십 개를 하나씩 보내면 게임 스레드를 수십 번 기다려야 합니다. `with g.batch():`로 감싸면 한 번만 기다립니다.
## 경제와 생산
### 10. 금광 하나에 일꾼은 최대 5명
더 늘려도 수입이 늘지 않습니다. 일꾼 목표는 금광 수에 맞춥니다. 금광마다 채금 5명에 벌목 몇 명을 더합니다.
### 11. 훈련 대기열에는 1개만
7칸을 다 채우면 돈이 대기열에 묶입니다(실측: 본진 건물에 일꾼 4명을 예약해 금 300이 묶였고, 초반이 크게 느려졌음). `g.queue(b)`가 비면 다음 것을 넣으세요.
### 12. 식량 막힘은 생산 테이블로 확인하기
`g.production(b).blocked` = 대기열에 있지만 시작되지 않음이며, 대개 식량 부족이 원인입니다. "식량이 거의 찼을 때 짓기"보다 한발 빠릅니다. 교전으로 병력을 한꺼번에 잃고 보충하다가 대기열이 막히면 바로 알 수 있습니다.
### 13. 영웅은 유일하고, 본진 대기열이 비어 있지 않으면 티어 업 불가
- 영웅이 죽으면 `g.revive(제단)`으로만 되살릴 수 있고, 다시 훈련하면 거부됩니다(221). 부활에도 식량이 필요합니다(영웅은 5).
- 본진 건물 대기열에 무언가 남아 있으면 본진 건물을 업그레이드할 수 없습니다(사유 코드 185, "건물이 사용 중").
## 시간과 공간
### 14. 2배속에서는 실제 시간으로 기다리지 않기
게임 시간 3초를 기다리려면 `sleep(1.5)`가 아니라 `g.clock()`이 3 늘었는지 보세요. 배속에서는 엔진 시계가 실제 시간보다 빨리 흐릅니다.
### 15. 섬 맵과 숲 맵에서는 직선 거리를 쓰지 않기
크립 캠프나 멀티 자리를 고를 때는 `g.path_distance(a, b)`(지상 A*, 숲·절벽·건물을 우회)를 쓰세요. 갈 수 없으면 `None`을 반환합니다. 직선으로 가장 가까운 지점이 바다 건너편일 수도 있습니다.
## 하나 더: 공정 모드 기준으로 작성하기
`--fair`에서는 시야 안의 유닛, 아이템, 생산, 이벤트만 보이며, 아레나 규칙도 이와 같습니다. 지금부터 공정 모드 기준으로 작성하면 나중에 [아레나](https://war3ai.com/ko/arena/)에 올라갈 때 고칠 필요가 없습니다. 자세한 내용은 [공정 모드](https://war3ai.com/ko/docs/fair-mode/)를 참고하세요.
---
# 공정 모드
> 게임에 주입된 클라이언트는 맵 전체를 읽을 수 있습니다. 공정 모드는 Bot이 시야 안의 것만 보게 합니다. 사람 플레이어와 같고, 아레나 규칙과도 같습니다.
이 프로젝트의 관찰 능력은 "클라이언트가 모든 플레이어의 상태를 가지고 있다"는 데서 나옵니다. 스냅샷에는 전장의 안개 속 적을 포함해 맵 전체의 모든 유닛이 들어 있습니다. 디버깅에는 편리하지만 대전에서는 공정하지 않습니다.
**공정 모드**는 SDK가 여러분의 시야를 기준으로 필터링하게 합니다.
```bash
python tools/play.py --bot my_bot.py --fair
python -m openwar3 run my_bot.py --inst 5 --fair
```
```python
from openwar3 import Game, run
g = Game(inst=5, fair=True) # Game을 직접 사용
run(MyBot, inst=5, fair=True) # 또는 러너에 맡기기
```
## 필터링되는 것
| 내용 | 공정 모드에서 |
|---|---|
| 유닛 | 아군 전체 + 지금 아군에게 보이는 적과 중립 유닛 |
| 바닥의 아이템 | 아군 유닛 시야 안의 것만(낮 / 밤 시야는 데이터 테이블에 따라 따로 계산) |
| 생산 테이블 | 보이는 건물만(상대가 무엇을 생산하는지 보이지 않음) |
| 이벤트 | 자신의 것, 보이는 것(또는 1초 안에 보였던 것), 아군이 입힌 피해 |
## 시야는 어디서 오는가
- 스냅샷의 모든 유닛에는 **가시성 마스크**가 붙어 있습니다. p번째 비트 = 플레이어 p가 지금 그 유닛을 볼 수 있음(필드에 유닛이 있는 0 ~ 11번만 계산하며, 자기 유닛은 자신에게 항상 보임). `u.visible_to(g.me())`는 이 값을 바로 읽으므로 대기 시간이 없습니다.
- 임의의 지점은 `g.visible(x, y)`로 엔진에 묻습니다(보임 / 전장의 안개 / 검은 마스크). 고속 레인을 거치므로 매번 약 1프레임이 걸립니다. 한 틱에 많은 유닛을 판단해야 한다면 `g.visible()`을 하나씩 호출하지 말고 스냅샷의 `u.visible_to()`를 쓰세요.
## 적에 대한 기억: `last_seen`
사람 플레이어는 "방금 저쪽에서 늑대 기수 부대를 봤다"는 것을 기억합니다. SDK도 대신 기억해 줍니다. 스냅샷을 새로 고칠 때마다 지금 아군에게 보이는 적과 크립 유닛을 기록하고(마지막 위치, 체력, 시각), 죽는 것을 보면 지우며, 게임이 바뀌면 비웁니다.
```python
for u, t, age in g.last_seen(max_age=60): # 60게임초 안에 본 적
print(u.type, u.x, u.y, f"{age:.0f}s 전")
heroes = [r for r in g.last_seen() if r[0].is_hero] # 상대 영웅을 마지막으로 본 위치
camps = g.last_seen(owner="creep") # 본 적 있는 크립
```
공정 모드에서는 이것이 유일한 "상대 정보 출처"입니다. 사람 플레이어와 같습니다. 일반 모드에서도 시야 기준으로 기록하므로 같은 코드를 그대로 쓸 수 있습니다.
## 몇 번 플레이어로 지휘할지
```bash
python tools/play.py --bot my_bot.py --player 1 --attach
```
`--player N`(또는 `Game(player=N)`)은 Bot이 N번 플레이어로서 지휘하게 하며, N번 플레이어의 유닛만 지휘할 수 있습니다. 두 AI를 대결시키려면 같은 게임에서 이런 채널을 두 개 열면 됩니다.
> **로컬 모드에서 공정성은 약속이지 보안 경계가 아닙니다**
>
> 여러분의 컴퓨터에서는 프로그램이 맵 전체를 읽는 것을 막을 방법이 없습니다. `--fair`는 스스로에게 거는 제약입니다. 진짜 대전은 [아레나](https://war3ai.com/ko/arena/)의 심판 프로세스가 보장합니다. Bot은 공유 메모리에 절대 접근할 수 없고, 심판이 시야 기준으로 필터링한 관찰만 받으며, 행동만 제출할 수 있고, 모든 행동은 먼저 유닛 소유권을 검증받습니다.
## 지금 켜야 하는 이유
- 나중에 아레나에 올라가면 규칙이 바로 이렇습니다. 지금 공정 모드로 작성해 두면 그때 한 줄도 고칠 필요가 없습니다.
- 맵 전체 정보 없이 싸워 봐야 Bot의 실제 실력을 알 수 있습니다(레퍼런스 브레인은 현재 컴퓨터 부대장의 목표 지점처럼 맵 전체 정보에 크게 의존합니다. 좋은 검증 기회입니다).
- 공정 모드에서 작성한 정찰, 기억, 판단이야말로 진짜 가치 있는 AI 능력입니다.
---
# 프로 운영 레시피
> 고수의 강점은 대부분 수십 가지 "작은 습관"에서 나옵니다. 이 페이지는 흔히 쓰는 프로 운영을 하나씩 SDK 코드로 옮겼으며, 각 코드는 그대로 on_tick에 붙여 넣을 수 있습니다.
약속: `g`는 `Game`, `home`은 아군 본진(`g.my_buildings({"htow", "hkee", "hcas"})[0]`), `now = g.clock()`입니다. API 세부 사항은 [API 목록](https://war3ai.com/ko/api/)을, 완전히 실행 가능한 예제는 [예제 Bot](https://war3ai.com/ko/docs/examples/)을 참고하세요.
> **팁**
>
> LLM에게 어떤 운영을 추가하게 할 때는 "좀 더 프로처럼 해 줘"라고 설명하는 것보다, 해당 항목을 코드와 함께 붙여 넣는 편이 훨씬 효과적입니다.
## 가. 운영
### 1. 농부는 절대 놀지 않고, 금광당 5명
```python
for w in g.idle_workers(): # 놀고 있는 일꾼에게만(일하는 일꾼에게 다시 명령하면 채집이 끊김)
mine = g.nearest([m for m in g.gold_mines() if crew[m.addr] < 5], w)
g.gather(w, mine) if mine else g.gather(w, g.trees(w.x, w.y, limit=1)[0])
```
금광마다 몇 명을 보냈는지(`crew`)는 직접 기록하세요. 금광에 들어간 일꾼은 스냅샷에 없습니다. 전체 예제는 `hello_bot.py`에 있습니다.
### 2. 대기열에는 1개만, 돈이 묶이지 않게
```python
for b in g.my_buildings({"hbar"}):
if not g.queue(b): # 비었을 때만 다음 것을 넣음
g.train(b, "hfoo")
```
### 3. 인구가 절대 막히지 않게
```python
stuck = any(p.blocked for _b, p in g.all_production("me")) # 대기열은 있는데 시작 못 함 = 인구 부족
res = g.resources()
if stuck or res["food_cap"] - res["food_used"] <= 6:
g.build_near(builder, "hhou", home.x, home.y)
```
`blocked`는 "거의 찼다"보다 한발 빠릅니다. 교전에서 병력을 한꺼번에 잃고 다시 보충할 때 대기열이 막히는 순간 바로 알 수 있습니다.
### 4. 빌드 순서 + 다 지으면 알아서 채광 복귀(Shift 복귀)
```python
spot = g.build_near(w, "hbar", home.x, home.y)
if spot:
g.gather(w, mine, queue="after") # 다 지으면 채광으로 복귀, 다음 틱에 다시 찾을 필요 없음
```
농부 한 명이 여러 채를 연달아 짓기: `g.build_queue(w, [("hhou", x1, y1), ("hhou", x2, y2)])`. 비용은 착공할 때 차감됩니다.
### 5. 테크 업 타이밍, 공격/방어 업그레이드
```python
if not g.queue(hall) and g.can_do(hall, "hkee") in (0, 220): # 본진 건물 대기열이 비어야 업그레이드 가능(아니면 185)
g.upgrade(hall, "hkee")
p = g.production(hall) # 테크 업 진행도
if p and p.kind == "upgrade":
print(f"성채까지 {p.remaining:.0f}초 남음")
for sm in g.my_buildings({"hbla"}):
if not g.queue(sm):
ok = [u for u, v in zip(UPS, g.can_do_many([(sm, u) for u in UPS])) if v in (0, 220)]
if ok:
g.research(sm, ok[0])
```
### 6. 확장: 걸어가는 거리로 가장 가까운 금광 고르기
```python
mines = [m for m in g.gold_mines() if g.dist(m, home) > 1500 and not taken(m)]
best = min(mines, key=lambda m: g.path_distance(home, m) or 1e9) # 섬의 금광은 None 반환 -> 맨 뒤로
```
## 나. 정찰과 정보
### 7. 상대가 뭘 하는지 보기
```python
for b, p in g.all_production("enemy"): # 보이는 상대 건물이 무엇을 훈련 / 연구 / 업그레이드하는지
print(b.type, p.kind, p.queue, f"{p.progress:.0%}" if p.progress is not None else "막힘")
```
이벤트와 함께 쓰기: `ev.kind == "production.done" and ev.owner != g.me()` — 상대가 방금 무엇을 뽑았는지.
### 8. 본 것을 기억하기(전장의 안개)
```python
for u, t, age in g.last_seen(max_age=60): # 60 게임 초 안에 본 적(마지막 위치와 체력)
...
hero_seen = [r for r in g.last_seen() if r[0].is_hero] # 상대 영웅을 마지막으로 본 곳
```
공정 모드에서는 이것이 상대에 대한 유일한 정보원입니다. 사람 플레이어와 똑같습니다.
### 9. 컴퓨터 상대가 어디를 치려는지(컴퓨터 AI에만 유효)
```python
plan = g.enemy_ai_plan(some_enemy_soldier) # 이 유닛의 컴퓨터 대장이 가려는 곳
```
컴퓨터는 출발하기 전에 목표 지점을 정해 둡니다 — 미리 그곳으로 병력을 데려가세요.
## 다. 사냥
### 10. 밤 사냥
```python
if g.is_night(): # 18시 ~ 6시: 크립이 잠듦(먼저 쳐도 포위당하지 않음), 모든 유닛의 시야가 짧아짐
...
wait = g.seconds_until(18) # 해가 지기까지 남은 게임 초(하루 480초)
```
### 11. 이길 수 있는 캠프만 치기
```python
from openwar3 import combat
mine = [g.stats(u) for u in army]
def ttk(target): return combat.time_to_kill(mine, g.stats(target), target_hp=target.hp) or 1e9
camp = [c for c in g.creeps() if g.dist(c, center) < 600]
ours = max(ttk(c) for c in camp) # 이 캠프를 전멸시키는 데 걸리는 시간(대략)
theirs = g.time_to_kill(camp, weakest_of_mine) or 1e9 # 크립들이 아군의 가장 약한 유닛을 죽이는 데 걸리는 시간
if ours < theirs and g.reachable(center, camp[0]):
g.attack_move(army, camp[0].x, camp[0].y)
```
전체 예제: `micro_bot.py`의 `_maybe_creep`.
## 라. 마이크로 컨트롤
### 12. 점사: 가장 가까운 적이 아니라 "가장 빨리 죽일 수 있는" 적을
```python
target = min(visible_enemies, key=lambda e: g.time_to_kill(fighters, e) or 1e9)
g.attack([u for u in fighters if (g.current_target(u) or target).handle != target.handle], target)
```
"그 대상을 공격하고 있지 않은" 유닛에게만 명령하고(`current_target`), 이미 공격 중인 유닛은 방해하지 마세요.
### 13. 체력 낮은 유닛 빼기
```python
for u in army:
if u.hp < u.hp_max * 0.35:
g.move(u, *toward(home, u, 500)) # 본진 방향으로 500 후퇴, 3초 안에 다시 빼지 말 것
```
점사당하고 있는지 판단하기: `damage` 이벤트에서 같은 유닛이 짧은 시간에 여러 공격자에게 맞으면 = 포위된 것입니다.
### 14. 영웅 생존, 경험치 헌납 금지
```python
for h in g.my_heroes():
if h.hp < h.hp_max * 0.4:
g.move(h, home.x, home.y)
g.use_item(h, slot_of(h, "phea")) # 치유 물약: inventory(h)로 칸 번호 찾기
```
### 15. 상성: 맞는 유닛이 맞는 대상을 치게
```python
s = g.stats(u)
best = max(enemies, key=lambda e: s.dps_vs(g.stats(e))) # 소총병은 그리폰 라이더를(관통 → 소형 갑옷 ×2), 그리폰 라이더는 보병을(마법 → 대형 갑옷 ×2)
```
상성표는 게임 데이터에서 가져옵니다: `combat.damage_multiplier("pierce", "small") == 2.0`.
### 16. 협공과 경로: 타워를 돌아서 가기
```python
route = g.walk_path(army_center, target) # 지상 최단 경로의 꺾이는 지점
g.path(army, route, attack=True) # 각 지점을 순서대로 공격 이동
```
타워를 피하려면 경로 탐색 그리드에서 타워 주변을 이동 불가로 표시한 뒤 계산합니다:
```python
grid = g.grid().copy()
for t in towers:
grid.block_area(t.x, t.y, 800) # 타워 사거리 700 + 여유분
route = grid.path((army_x, army_y), (target.x, target.y))
```
### 17. 한 틱의 명령은 한 배치로
```python
with g.batch():
g.attack(melee, target_a)
g.attack(ranged, target_b)
g.move(wounded, *home_xy)
g.cast(hero, "thunderclap")
```
명령 수십 개도 게임 스레드를 한 번만 기다립니다(실측: 이동 명령 8개 68 ms → 6.5 ms).
### 18. 공성: 포로 지면 공격
```python
g.attack_ground(mortars, tower.x, tower.y) # 박격포 부대 / 투석기가 한 지점에 포격(숲 뒤, 은신 유닛)
```
## 마. 영웅
### 19. 스킬 찍는 순서
```python
SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # 물의 정령, 눈보라…… 6레벨 궁극기 대규모 순간이동
info = g.hero_info(h)
if info and info["skill_points"]:
g.learn(h, SKILLS[h.type][learned_count]) # 거부되면(궁극기를 배울 레벨이 아님) 다음 레벨까지 기다림
```
### 20. 스킬이 실제로 나갔는지
```python
r = g.cast(h, "thunderbolt", target=enemy_hero)
# 다음 틱:
if g.cooldown(h, "AHtb"): # 쿨다운 진입 = 실제로 시전됨. 수락 ≠ 시전
...
```
### 21. 물약 구매, 귀환
```python
g.buy(shop, "phea") # 영웅이 상점 옆에 서 있어야 함
g.use_item(hero, slot, x=home.x, y=home.y) # 마을 귀환 주문서(지점 대상 아이템 사용)
```
## 바. 리뷰
- 매 틱 결정을 로그로 남기고(`print`는 실행 창에 출력됩니다), `g.say(유닛, "후퇴")`와 함께 게임 안에서 확인하세요.
- `production.done` 이벤트에는 "걸린 시간(초)"이 들어 있습니다 — 자신의 빌드 타임라인(첫 영웅이 몇 초에 나왔는지, 몇 초에 테크 업을 했는지)을 집계해 고수와 비교하세요.
- Agent가 스스로 리뷰하게 하기: [Agent 자율 반복](https://war3ai.com/ko/docs/agent-loop/)의 게임 리포트를 참고하세요.
---
# 예제 Bot
> 단순한 것부터 복잡한 것까지 네 가지 예제입니다. 모두 바로 실행할 수 있고, 로직 하나하나가 SDK 기능 하나에 대응합니다. 완전한 레퍼런스 브레인도 함께 제공합니다.
예제는 모두 `brains/examples/`에 있으며, 뒤의 예제가 앞의 예제를 상속해 새로운 것만 더합니다. 순서대로 읽기를 권장합니다.
| 예제 | 배우는 내용 | 실행 |
|---|---|---|
| `hello_bot.py` | 채집(금광 하나에 5명, 금광이 차면 벌목), 일꾼 생산(대기열에 1개만), 식량 건물 건설, 멈춘 건설 부지 이어 짓기. 네 종족 모두 실행 가능 | `python tools/play.py --bot brains/examples/hello_bot.py` |
| `rush_bot.py` | 병영과 제단(없으면 `build_near`로 건설), 영웅 우선 생산(죽으면 부활), 스킬 포인트가 있으면 습득, 한 웨이브가 모이면 공격 이동 | `… --bot brains/examples/rush_bot.py` |
| `macro_bot.py` | 건설 순서 + 다 지으면 알아서 금광 복귀(Shift), 식량이 막히면 즉시 보충, 병영 대기열 1개, 공격/방어 업그레이드, 티어 업과 고급 유닛, **실제 지상 이동 거리**로 목표를 고르고 경로를 따라 이동 | `… --bot brains/examples/macro_bot.py --speed 200` |
| `micro_bot.py` | 운영 위에서 전투를 맡음: 가장 빨리 잡을 수 있는 적 점사, 체력 낮은 유닛 빼기, 영웅 생존, 밤에 이길 수 있는 크립 캠프 골라 사냥, 적이 본진 앞까지 오면 수비 복귀. 한 틱의 명령은 한 배치로 전송 | `… --bot brains/examples/micro_bot.py --fair` |
> **참고**
>
> `hello_bot`과 `rush_bot`의 주석에는 실제 게임에서 겪은 함정이 기록되어 있습니다. 예를 들어 "매번 첫 번째 일꾼에게 건설을 맡겼더니 농장 3채가 모두 반쯤 지은 건설 부지로 남았다", "하드코딩한 병영 좌표가 하필 숲이라 3분 동안 한 채도 짓지 못했다" 같은 것들입니다. 코드보다 주석을 읽는 편이 얻는 것이 더 많습니다.
## hello_bot: 경제
```python
# 종족 -> (일꾼, 본진 건물들, 식량 건물)
RACES = {
"h": ("hpea", {"htow", "hkee", "hcas"}, "hhou"),
"o": ("opeo", {"ogre", "ostr", "ofrt"}, "otrb"),
"u": ("uaco", {"unpl", "unp1", "unp2"}, "uzig"),
"e": ("ewsp", {"etol", "etoa", "etoe"}, "emow"),
}
MINE_CAP = 5 # 금광 하나에 일꾼 최대 5명(더 늘려도 수입이 늘지 않음)
LUMBER_CREW = 5 # 벌목 인원: 금광마다 채금 5명 + 이 인원 = 일꾼 목표
```
세 가지를 합니다. 쉬고 있는 일꾼은 금을 캐러 보냅니다(금광마다 몇 명인지 직접 기록하고, 금광이 차면 벌목하러 보냄). 일꾼이 부족하면 생산합니다(대기열에는 1개만). 식량이 거의 차면 건설 중이 아닌 일꾼을 찾아 본진 건물 옆에 식량 건물을 짓습니다(휴먼과 오크는 멈춘 건설 부지에 일꾼을 보내 이어 짓기도 함).
## rush_bot: 병력 생산과 출격
`hello_bot`에 세 가지를 더합니다. 병영과 제단이 없으면 짓습니다. 제단에서 영웅을 뽑고(**죽었으면 먼저 부활**, 영웅은 유일함), 스킬 포인트가 있으면 배웁니다. 병력이 8기 모이면 전원을 적 본진 건물로 공격 이동시키고, 병력이 크게 줄면 본진으로 돌아와 다시 모읍니다. 틱마다 전투를 끊지 않도록 쉬고 있는 병력에게만 명령합니다.
## macro_bot: 운영 기본기
```python
TECH = {
"h": dict(order=["halt", "hbar", "hbla", "hlum"], altar="halt", hero="Hamg", skills=["AHwe", "AHbz", "AHab"],
barracks="hbar", soldiers=["hfoo", "hrif", "hkni"], smith="hbla", upgrades=["Rhme", "Rhar", "Rhra", "Rhla"],
tiers=["hkee", "hcas"]),
...
}
```
프로 선수가 매 게임 하는 몇 가지 일이며, 각각이 SDK 기능 하나에 대응합니다. 건설 순서표 + `gather(..., queue="after")`로 다 지으면 금광 복귀, `production().blocked`로 식량 막힘 감지, `g.queue`로 병영 대기열을 1개로 유지, `can_do`로 다음 단계 공격/방어 업그레이드를 연구할 수 있는지 엔진에 질의, 티어 업과 고급 유닛(실게임 교훈: 계속 1티어에 머물다가 23분에 3티어 기사와 그리폰에게 밀려 전멸), `path_distance`로 목표 선택, `path()`로 경유점을 따라 이동.
## micro_bot: 전투가 시작된 뒤
```python
def _fight(self, g, army, foes, home, now):
...
visible = [e for e in foes if e.visible_to(me)] # 보이지 않는 대상은 거부됨(1001)
atk = [s for s in (g.stats(u) for u in fighters) if s]
target = min(visible, key=lambda e: _ttk(g, atk, e)) # 가장 가까운 적이 아니라 가장 빨리 잡을 수 있는 적
idle_or_other = [u for u in fighters if g.current_target(u) is None
or g.current_target(u).handle != target.handle]
if idle_or_other:
g.attack(idle_or_other, target)
```
실게임: 5분 동안 1497틱, 명령 3023개, 오류 0건.
## 레퍼런스 브레인: 완전한 AI
`brains/xwar3/`는 멀티를 가져가고, 크립을 사냥하고, 출격까지 하는 완전한 AI이며, 세 계층으로 나뉩니다.
| 계층 | 위치 | 주기 | 하는 일 |
|---|---|---|---|
| 전략 레이어 | `strategy/` | 초 단위 | AMAI 방식의 다중 전략 선택과 전환, 건설표, 카운터 유닛, 영웅 선택. 선택 사항으로 [LLM 운영 코치](https://war3ai.com/ko/docs/llm-coach/) |
| 반사 레이어 | `reflex/`(독립 프로세스 4개) | 100 ms 단위 | 생존, 시전, 점사, 장비 줍기 |
| 승률 모델 | `worldmodel/` | — | 싸워서 이길 수 있는지 판단(추론 서브셋) |
여러 프로세스가 **점유 테이블**로 유닛을 공유하고, 우선순위에 따라 누구의 명령을 따를지 정합니다: 사람 95 > 생존 90 > 스킬 회피 85 > 시전 80 > 장비 줍기 70 > … > 전략 50 > 작업 배정 45. 여러분의 Bot은 테이블에서 `bot`이라는 신분을 쓰며, 기본 우선순위는 50입니다.
> **주의**
>
> 레퍼런스 브레인은 SDK의 저수준 계층(`w3cmd` / `act`)을 직접 사용하고, 맵 전체 정보에 크게 의존합니다. "아이디어" 참고용으로는 좋지만 LLM이 그대로 베끼게 하는 것은 권장하지 않습니다. 레퍼런스 브레인에는 AMAI 데이터가 필요합니다. `start.bat`이 처음 배포할 때 AMAI의 공개 저장소에서 받아와 생성합니다(AMAI는 자체 라이선스이며, 생성물은 git에 포함되지 않습니다. 실패했다면 `start.bat setup`으로 다시 시도하세요).
레퍼런스 브레인을 시작하는 가장 쉬운 방법은 [Farsight 콘솔](https://war3ai.com/ko/docs/console/)을 쓰는 것입니다. "인스턴스와 게임 시작" 페이지에서 인스턴스 번호를 체크하고 "테스트 시작"을 누르세요.
---
# 디버깅과 성능
> 틱이 왜 느린지, 명령이 왜 먹히지 않는지, 게임이 왜 멈춰 있는지. 증상별로 점검하고 내장된 실게임 검증 스크립트로 확인합니다.
## 회신 확인하기
모든 명령의 회신이 가장 직접적인 단서입니다.
```python
r = g.cast(hero, "blizzard", x=tx, y=ty)
if not r:
print(r.reason, r.verdict) # rejected(…)와 사유 코드
print(r.exec_us, r.engine_us) # 이 명령이 게임 스레드에서 실행된 마이크로초 / 그중 엔진의 명령 함수 자체가 쓴 시간
```
정상이라면 명령 하나가 게임 스레드에서 몇 마이크로초에서 수백 마이크로초가 걸립니다. 배치 블록이 끝나면 `g.last_receipts`에 이번 배치의 명령별 회신이 들어 있습니다.
## 게임 안에서 보기
```python
g.say(unit, "후퇴") # 유닛 머리 위에 채팅 말풍선 표시(게임에 영향 없음)
g.message("크립 사냥 시작") # 왼쪽 아래 메시지 영역에 한 줄 출력(로컬에서만 보임)
```
`print`한 내용은 Bot을 실행하는 터미널에 출력됩니다. 매 틱의 핵심 결정을 출력하고 머리 위 말풍선과 함께 보면 코드를 읽는 것보다 훨씬 빠릅니다.
## 틱이 느릴 때
먼저 다음 경우에 해당하는지 확인하세요.
| 원인 | 해결 방법 |
|---|---|
| 명령을 하나씩 보내 매번 한 프레임씩 기다림 | `with g.batch():`로 감싸면 수십 개도 한 번만 기다림 |
| `g.visible()` / `g.can_do()`를 하나씩 호출(매번 고속 레인으로 한 프레임 대기) | 가시성은 스냅샷의 `u.visible_to()`로, 실행 가능성은 `g.can_do_many([...])`로 한꺼번에 질의 |
| `on_tick` 안에서 `sleep`하거나 대기 | 게임 시간을 기록해 두고 다음 틱에 다시 판단 |
| 비싼 계산(경로 탐색, 전체 맵 스캔)을 매 틱 다시 수행 | 결과를 캐시하고 몇 틱마다 다시 계산. `g.grid()`는 자체 2초 캐시가 있고, `g.stats()`의 기술 레벨은 5초에 한 번 캐시 |
## 게임이 멈춤 / Bot이 게임 진입을 기다리기만 함
| 증상 | 대개 원인 |
|---|---|
| 계속 "게임 진입 대기" | 인스턴스 번호가 틀림. 또는 게임 창이 **최소화**됨 — 최소화하면 게임 시뮬레이션이 멈춤(시계가 흐르지 않음) |
| 게임은 돌아가는데 Bot 명령에 반응이 없음 | 다른 플레이어의 유닛에 명령 중(회신 `not_owner`). 또는 Bot이 observer 역할로 연결됨(`forbidden`) |
| 명령이 `held`됨 | 더 높은 우선순위의 레이어(레퍼런스 브레인의 반사 레이어, 콘솔의 수동 명령)가 이 유닛을 점유하고 있어 전송되지 않음 |
| 일시 정지 후에도 명령이 내려짐 | 정상: 일시 정지 중에는 엔진 시계가 멈추지만, 이벤트 디스패치는 계속 돌고 명령도 그대로 실행됨 |
## 연결해서 상태 보기
```bash
python -m openwar3 status --inst 5
```
연결 상태를 출력합니다. 게임 pid, 월드 발행 주기와 수집 1회당 소요 시간, 고속 레인 카운터, 게임 중인지 여부, 유닛 수, 게임 시계.
## 실게임 검증 스크립트
테스트 인스턴스를 하나 띄우고, SDK 기능이 여러분의 컴퓨터에서 정상 동작하는지 항목별로 확인합니다.
```bash
python tools/sdk_live_check.py --inst 20 # 전체
python tools/sdk_live_check.py --inst 20 --only prod # 한 섹션만 검증
```
섹션은 배치, 시간, 생산, 대기열 명령, 전투 속성, 경로 탐색, 공정 모드입니다. 각 섹션은 실제 대전에서 명령을 내리고 효과를 다시 읽어 통과 개수를 출력합니다.
오프라인 테스트는 게임을 띄울 필요가 없습니다.
```bash
python tools/run_tests.py
```
## 흔한 "버그처럼 보이는 것"
- **건설 회신은 수락됐는데 건설 부지가 끝내 생기지 않음**: 숲속 지점도 엔진은 그 자리에서 수락하고, 일꾼이 도착해서야 실패합니다. `build_near`를 쓰세요. 실패한 지점을 추적해 한동안 블랙리스트에 올립니다.
- **스킬 회신은 수락됐는데 시전되지 않음**: 끊겼거나 마나가 부족합니다. 시전 후 다음 틱에 `g.cooldown()`이 쿨다운에 들어갔는지 확인하세요.
- **공격 명령이 수락됐는데 병력이 다른 적을 공격함**: 특정 대상을 공격하려면 `g.attack(병력, 적)`(우클릭과 같은 의미)을 써야 합니다. 원시 공격 오더는 대상에 대해 오더만 바꾸고 대상을 기록하지 않아서, 근처의 다른 적을 공격합니다.
- **일꾼 수가 맞지 않음**: 금광에 들어간 일꾼은 스냅샷에 없습니다.
- **죽은 영웅을 훈련할 수 없음**: 영웅은 유일하므로 `g.revive(제단)`을 써야 합니다. 부활에는 식량이 필요하고, 사망 후 약 3게임초가 지나야 부활할 수 있습니다.
---
# RPG 동료
> RPG / 커스텀 맵에서 플레이어에게 AI 동료를 붙여 줍니다. 따라다니고, 사냥을 돕고, 체력이 낮으면 치유해 주고, 말동무가 되어 줍니다. 네 가지 모드가 있으며, 클래스 하나를 상속하고 속성 몇 개만 바꾸면 나만의 동료가 됩니다.
대전만이 아닙니다. RPG나 커스텀 맵에서 나만의 **AI 동료**를 둘 수 있습니다. 동료는 여러분을 따라다니며 사냥을 돕고, 체력이 낮아지면 치유해 주고, 한가할 때는 말을 건넵니다 — 대사는 로컬 LLM에 연결할 수도 있습니다.
**어떻게 쓸지는 여러분이 정합니다.** 이 기능은 세 층의 인터페이스로 나뉘어 있으며, 아래층부터 위층까지 어느 층이든 바로 쓸 수 있습니다.
| 층 | 무엇인가 | 적합한 경우 |
|---|---|---|
| **JASS 채널** `g.jass` | 맵 제작자가 쓸 수 있는 JASS 함수 1291개를 이름으로 바로 호출(유닛 생성, 동맹 설정, 아이템 지급, 이름 변경, 텍스트 표시, 영웅 부활……) | 게임플레이를 직접 만들고 싶을 때 |
| **편의 API** | `g.spawn`, `g.set_alliance`, `g.player_slots`, `g.show_text`, `g.map_data`: 자주 쓰는 작업을 묶어 둠 | 나만의 보조 스크립트를 쓸 때 |
| **동료 프레임워크** | `openwar3.companion.Companion` + `openwar3.talk.Talk`: 상속하고 속성 몇 개만 바꾸면 따라다니고, 전투를 돕고, 치유하고, 대화하는 동료가 완성됨 | 동료가 필요할 때 |
> **주의**
>
> **싱글플레이 또는 LAN에서 직접 만든** 게임에서만 사용하세요. 유닛 생성이나 동맹 설정 같은 작업은 이 컴퓨터가 일방적으로 월드를 바꾸는 것입니다. 싱글플레이 게임(컴퓨터 상대)은 문제없지만, 멀티플레이 게임에서는 다른 플레이어와 동기화가 깨집니다. 그래서 멀티플레이 게임에서는 JASS 채널이 읽기 전용 함수만 허용하고, 동료는 자동으로 "말만 하기"로 물러납니다.
## 가장 빠르게 시작하기: Farsight에서 바로 켜기
1. **맵 선택**: Farsight "인스턴스" 페이지 → "다음 게임 설정" → 맵에서 RPG 맵을 고릅니다(게임 폴더 `Maps` 아래 `Scenario`, `Download`에 있는 맵이 모두 표시됩니다. 예: `(4)WarChasers`).
2. **스킴 선택**: 인스턴스 카드의 "AI 스킴" 드롭다운에서 **동료 예제**(buddy)를 고르고 → "선택"을 누릅니다.
3. **테스트 시작**: 게임이 뜨면 **게임 창에서 직접 플레이하세요**. 동료 — "빛나"라는 이름의 팔라딘 — 가 여러분 곁에 나타납니다.
명령줄로도 할 수 있습니다.
```bash
python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<게임 폴더>\Maps\Scenario\(4)WarChasers.w3m"
```
`--rpg`(스킴 매니페스트에서는 `"judge": false`)는 대전 규칙으로 승패를 판정하지 않는다는 뜻입니다. RPG에서는 영웅이 죽어도 부활할 수 있고, "건물이 전부 없어지면 패배"라는 규칙도 없습니다. 많은 RPG 맵은 로딩이 끝나면 "계속하려면 아무 키나 누르세요" 화면에서 멈춥니다. SDK는 "게임 중인데 게임 시계가 계속 0"인 상태를 발견하면 스스로 스페이스 키를 한 번 누릅니다(`g.press_to_continue()`. 게임 창에 키 메시지만 보내며 포커스를 빼앗지 않습니다).
## 나만의 동료 만들기
```python
from openwar3.companion import Companion
from openwar3.talk import Talk
class MyBuddy(Companion):
mode = "ally" # 모드, 아래 표 참고
unit = "Hpal" # 무엇을 만들지: 아무 4자 코드나 가능, 맵 커스텀 유닛도 가능
nickname = "빛나"
heal = ("holybolt", "AHhb", 0.55) # (시전 오더 이름, 배울 스킬, 주인 체력이 이 비율 아래면 치유); None = 치유 안 함
follow_distance = 350
talk = Talk(persona="명랑한 꼬마 팔라딘, 주인을 응원하길 좋아함")
```
### 네 가지 모드
| mode | 동료는 누구인가 | 설명 |
|---|---|---|
| `ally`(기본값) | 빈 플레이어 슬롯 하나를 차지해 여러분의 **동맹**이 됨 | 자기 색과 이름이 있음(점수판, 동맹 패널에 `nickname` 표시). 여러분이 명령할 수는 없고 스스로 싸움. 프레임워크가 자동으로 동맹 + 시야 공유로 설정 |
| `own` | **여러분 소유**로 생성 | 언제든 직접 지휘할 수 있음. 여러분이 손대지 않을 때는 AI가 대신 조작 |
| `adopt` | 맵에 **이미 있는** 유닛을 넘겨받음 | `adopt(g)`를 재정의해 그 유닛을 반환(맵이 준 펫, 수행원) |
| `voice` | 유닛을 만들지 않고 **말만 함** | 말동무, 알림. 월드를 바꾸지 않으므로 멀티플레이 게임에서도 사용 가능 |
빈 슬롯이 없으면 `ally`는 자동으로 `own`으로 물러나고, 멀티플레이 게임이거나 유닛을 만들 수 없으면 자동으로 `voice`로 물러납니다.
> **참고**
>
> `ally` 모드의 동료는 유닛 하나에 고정되지 않고 "그 슬롯에서 지금 가장 좋은 유닛"(영웅 우선)을 자기 몸으로 삼습니다. 실측에서 어떤 맵은 동료를 실제 플레이어로 취급해 팔라딘을 지우고 맵 영웅을 하나 지급했습니다 — 동료는 그 영웅을 바로 넘겨받고, 맵이 그 영웅에게 정해 둔 스킬도 배웁니다. 영웅이 죽으면 우선 제자리에서 부활시키고, 맵이 알아서 부활시키면 그대로 이어서 씁니다.
### 매 틱 하는 일
순서대로 확인해, 조건이 맞는 항목을 실행합니다.
| 순서 | 행동 | 조건 | 조정 |
|---|---|---|---|
| 1 | 후퇴 | 자기 체력이 25% 미만이고 근처에 적이 있음: 주인 뒤로 물러남 | `retreat_at` |
| 2 | 치유 | 주인 체력이 설정값 미만, 스킬 쿨다운이 끝남, 거리 900 이내 | `heal`(None이면 끔) |
| 3 | 전투 지원 | 주인 주변에 적이 있음: **주인을 공격 중인 적 > 주인이 공격 중인 적 > 가장 가까운 적** | `assist_radius`, 또는 `pick_target` 재정의 |
| 4 | 따라가기 | 주인과 너무 멀어지면 따라감. 일정 거리 이상 멀어지면 싸움을 끊고 바로 돌아옴 | `follow_distance`, `leash` |
| 5 | 잡담 | 적이 없을 때 1 ~ 2.5분마다 한 마디 | 대사 표 |
"적"은 게임 안의 동맹 관계로 판단합니다(20초마다 갱신). RPG 맵에는 동맹이 여럿인 경우가 많아서, "나를 제외한 플레이어"를 전부 적으로 취급할 수는 없습니다.
재정의할 수 있는 훅: `find_master`(누가 주인인지. 기본값은 로컬 플레이어의 레벨이 가장 높은 영웅), `adopt`, `pick_target`, `on_poke`(주인이 동료를 우클릭함), 그리고 Bot의 `on_start` / `on_tick` / `on_event` / `on_end`. 치유, 전투 지원, 처치, 따라가기, 후퇴, 말하기, 부활 횟수는 모두 `self.stats`에 기록되고 끝날 때 출력됩니다.
### 부르는 법
- **채팅 명령**: 채팅창에 `-follow` 따라오기, `-stay` 제자리 지키기, `-heal` 즉시 치유, `-hi` 인사를 입력합니다. 명령 목록을 바꾸려면 `commands`를, 반응을 바꾸려면 `on_command`를 재정의하세요.
- **동료 우클릭**: `on_poke`가 호출됩니다. 예제의 반응은 이렇습니다. 주인의 체력이 가득 차 있지 않으면 한 번 치유해 주고, 그렇지 않으면 한 마디 합니다.
- **초상화 대사**: 인사, 주인 쓰러짐, 주인 레벨 업, 동료 복귀 때의 대사는 게임 자체의 초상화 대사로 말합니다(하단 초상화가 동료로 바뀌고 화면에 자막이 나옴). 나머지는 머리 위 말풍선으로 띄웁니다.
- **상태 패널**: 화면 왼쪽의 패널에 동료의 체력 바, 지금 하는 일, 기분(기쁨 / 흥분 / 긴장 / 두려움 / 슬픔), 처치 수와 치유 횟수를 표시합니다. [캔버스](https://war3ai.com/ko/docs/canvas/)로 그리므로 멀티플레이 게임에서도 안전합니다.
### 말하기, 그리고 로컬 LLM
`Talk`는 이벤트에 따라 대사를 골라 머리 위 말풍선으로 띄웁니다. `voice` 모드이거나 말풍선을 띄울 수 없으면 화면 왼쪽 아래에 표시합니다. 모든 대사는 스킴 로그에도 기록되므로, 나중에 무슨 말을 했는지 확인할 수 있습니다.
| 이벤트 | 언제 | 이벤트 | 언제 |
|---|---|---|---|
| `hello` | 막 도착했을 때 | `master_low` | 주인 체력이 낮을 때 |
| `poke` | 주인이 동료를 우클릭했을 때 | `master_levelup` | 주인 레벨 업 |
| `fight` | 전투 시작 | `master_died` / `master_back` | 주인 쓰러짐 / 부활 |
| `kill` | 몬스터 하나를 처치(몬스터 이름을 말함) | `buddy_low` / `buddy_died` / `buddy_back` | 동료 자신의 체력 낮음 / 쓰러짐 / 복귀 |
| `healed` | 주인을 치유함 | `idle` / `item` | 잡담 / 아이템 획득 |
대사에는 `{master}`, `{me}`, `{map}`, `{enemy}`, `{level}`, `{item}` 같은 자리표시자를 쓸 수 있습니다. 대사는 `talk.lines`에서, 쿨다운은 `talk.cooldown`에서 바꿉니다.
**로컬 LLM 연결**: `Talk(llm=LocalLLM(url, model))`. OpenAI 호환 API라면 무엇이든 됩니다(LM Studio, Ollama……). 모델은 백그라운드 스레드에서 답하고, 답이 도착해야 말합니다. 모델이 꺼져 있거나 시간이 초과되거나 오류가 나면 고정 대사를 말하므로 게임이 멈추지 않습니다. 요청은 여러분이 지정한 로컬 주소로만 보내며, 내용은 게임 안에서 일어난 일(주인 이름, 어떤 몬스터를 잡았는지)입니다.
## 커스텀 맵의 유닛 이름
RPG 맵의 유닛, 아이템, 영웅은 대부분 맵이 새로 만든 것이라(4자 코드가 `HC07`, `I00A` 같은 형태) 내장 이름표에서 찾을 수 없습니다. `g.map_data`는 현재 게임의 맵 파일을 직접 읽습니다.
```python
md = g.map_data
md.name_of("HC07") # 'Optimus Primo' — 맵에서 바꾼 이름이 우선
md.hero_names("HC07") # 칭호 목록
md.hero_skills("OC10") # 맵이 이 영웅에게 정한 스킬
md.tooltip("I00A") # 설명 텍스트
```
보호되거나 최적화된 맵(인기 RPG 중 상당수)에는 표준 오브젝트 데이터 파일이 없으므로, 이름을 맵 안의 텍스트 데이터에서 읽습니다. 실측 결과 이 컴퓨터에 있는 RPG / 커스텀 맵 38개를 모두 파싱했고, 그중 37개에서 유닛 이름을 얻었습니다.
## 스킴으로 만들어 공유하기
동료는 `openwar3.Bot`의 하위 클래스일 뿐이므로 [AI 스킴](https://war3ai.com/ko/docs/schemes/)으로 만들어 다른 사람과 공유할 수 있습니다. 매니페스트에 두 항목을 더 씁니다.
```json
{"id": "my-buddy", "name": "나의 동료", "entry": "my_buddy.py", "fair": false, "judge": false}
```
`"fair": false`: JASS 채널(유닛 생성, 동맹 설정)을 쓰기 위해 필요합니다. `"judge": false`: 대전 규칙으로 승패를 판정하지 않습니다.
## 실측 기록
2026-09-24, 테스트 인스턴스, WarChasers 맵, 2배속:
- JASS 채널 검사 18개 모두 통과: 플레이어 슬롯, 유닛과 핸들의 상호 변환, 실수 반환값, 문자열 인자, 빈 슬롯에 유닛 생성, 동맹 설정, 이름 변경, 유닛 삭제. 플레이어 레인에서 호출한 경우와 인자 개수가 틀린 경우는 올바르게 거부됨.
- 동료: 스스로 "계속하려면 아무 키나 누르세요"를 넘김 → 주인 곁에 나타나 인사 → 영웅 선택용 서클로 따라 들어갔다가 맵에서 영웅을 지급받아 넘겨받음 → 따라가기(주인과 200 ~ 400 거리) → 사냥, 한 마리를 잡고 "멋져!"라고 말함 → 체력이 낮아져 후퇴 → 전사 후 맵이 부활시켜 다시 따라감.
## 아직 안 된 것
1. **플레이어가 입력한 임의의 채팅 문장은 읽을 수 없습니다.** 정해진 채팅 명령은 이미 동작합니다. 동료가 정말로 자유롭게 대화하려면 문장 자체를 얻어야 합니다.
2. **동료는 특정 맵의 게임 방식(퀘스트, 상점, 스토리)을 모릅니다.** 범용적인 따라가기, 전투 지원, 치유만 합니다. 특정 맵을 이해시키려면 하위 클래스에서 그 맵에 맞게 작성하세요 — `g.map_data`로 이름을 찾고, `g.jass`로 어떤 함수든 호출할 수 있습니다. 바로 이 부분이 여러분이 직접 정하도록 남겨 둔 몫입니다.
---
# 캔버스
> 게임 화면에 텍스트 상자, 패널, 진행 바, 이미지, 지면에 붙는 원, 화살표가 달린 경로를 그립니다. 런타임이 매 프레임 직접 그리며 게임 상태를 바꾸지 않으므로 멀티플레이 게임에서도 안전합니다. Python, HTTP, 공유 메모리 직접 쓰기를 모두 지원합니다.
외부 프로그램이 게임 화면에 **텍스트 상자, 패널, 진행 바, 이미지, 지면의 원, 화살표가 달린 지면 경로**를 그릴 수 있으며, 런타임이 매 프레임 직접 그립니다. 나만의 HUD, 보조선, 안내, 교육용 표시, 방송 정보판을 만들기에 알맞습니다.
## 캔버스와 JASS 화면 함수, 어느 쪽을 쓸까
| | 캔버스(이 페이지) | [JASS 화면 함수](https://war3ai.com/ko/docs/jass/) |
|---|---|---|
| 그리는 주체 | 런타임이 직접 그림 | 게임 자체(떠다니는 텍스트, 특수 효과, 패널, 초상화 대사……) |
| 멀티플레이 게임 | **안전**: 이 컴퓨터의 화면에만 그리며, 게임 오브젝트를 만들지 않고 게임 상태도 바꾸지 않음 | 싱글플레이 게임만 가능 |
| 스타일 | 자유로움: CJK 글꼴, 둥근 모서리, 반투명, 테두리, 임의의 색, 로컬 이미지 | 게임 기본 스타일 |
| 대상 따라가기 | 유닛, 월드 좌표, 화면 위치를 따라감. 지면의 원은 지형 기복에 맞춰 붙음 | 함수마다 다름 |
| 비용 | 실측 프레임당 0.2 ~ 0.35 ms(요소 9개) | 호출 1회에 약 13 ms |
두 방법은 함께 쓸 수 있습니다. 게임 고유 스타일의 효과는 JASS로, 직접 디자인한 패널, 보조선, 안내는 캔버스로 그리세요.
## Python
```python
c = g.canvas # 처음 쓸 때 런타임이 그리기 훅을 설치(약 0.1초)
c.text("title", "안녕하세요, 캔버스입니다", screen=(40, 110), color=(255, 220, 80),
bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True)
c.panel("status", "동료 · 빛나", ["기분: 기쁨", "처치: 12"], screen=(16, 330))
c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # 유닛을 따라감
c.text("tag", "보스가 궁극기를 쓰려고 합니다!", unit=boss, lift=320, color=(255, 80, 80), size=22, bold=True)
c.circle("danger", (x, y), 300, color=(255, 60, 60), fill=(255, 60, 60, 60), width=3) # 지면의 위험 지역
c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # 유닛을 따라가는 원
c.path("route", [(x1, y1), (x2, y2), (x3, y3)], color=(255, 220, 0), width=5, arrow=True)
c.image("icon", "icon.png", screen=(40, 170), width=64, height=64)
c.remove("danger"); c.hide("tag"); c.clear() # clear는 자기가 그린 것만 지움
c.expire("tag", 5) # 5초 뒤 저절로 사라짐
with c.batch(): ... # 여러 개를 한 번에 바꾸고, 공유 메모리는 한 번만 씀
c.stats() # drawnFrames가 늘고 있으면 = 실제로 그리는 중
```
각 요소는 `key` 하나로 식별합니다. 같은 key로 다시 그리면 갱신됩니다.
**클릭 가능**: 텍스트 상자와 패널에 `clickable=True`를 붙이면(호버 색은 `hover=`로 지정) 클릭했을 때 이벤트 스트림에 `ui.click`이 하나 들어오고, `ev.key`가 바로 그 key입니다. 그 위를 누른 클릭은 게임에 전달되지 않습니다. 바로 쓸 수 있는 버튼, 선택 카드, 단축키, 지면 클릭은 [UI와 입력](https://war3ai.com/ko/docs/ui-input/)을 참고하세요.
**위치**(요소마다 하나 지정):
- `screen=(x, y)`: 화면 픽셀. 음수는 오른쪽 / 아래쪽에서부터 거꾸로 셉니다. `center=True`이면 중심 기준으로 정렬합니다.
- `frac=(0.5, 0.1)`: 화면 비율.
- `world=(x, y)`: 월드 좌표.
- `unit=유닛`: 유닛을 따라갑니다. 월드와 유닛 위의 텍스트, 진행 바는 아래쪽 가운데를 그 지점에 맞추며, `lift`만큼 위로 올립니다.
월드와 유닛 위의 요소는 기본적으로 하단 조작 패널과 상단 낮밤 시계를 피합니다(`over_ui=True`이면 그 위를 덮음). **색**은 `(r, g, b)`, `(r, g, b, a)`, `"#RRGGBB"`, `"#RRGGBBAA"`로 쓸 수 있습니다.
| 메서드 | 그리는 것 | 자주 쓰는 인자 |
|---|---|---|
| `text(key, 텍스트, ...)` | 텍스트 상자. 여러 줄은 `\n` | `color`, `bg` 배경색(없으면 투명), `border`, `size`, `bold`, `shadow`, `width`(이 너비에서 줄바꿈), `radius` 둥근 모서리 |
| `panel(key, 제목, [줄...], ...)` | 패널(어두운 반투명 배경, 금색 테두리) | `text`와 같음 |
| `bar(key, 0..1, ...)` | 진행 바: 체력, 쿨다운, 시전 바 | `width`, `height`, `color`, `bg`, `border`, `text` |
| `image(key, 경로, ...)` | 로컬 이미지(png / jpg / bmp / gif) | `width`, `height`(없으면 원본 크기) |
| `circle(key, 유닛 또는 지점, 반지름, ...)` | 지면의 원, 지형에 붙음 | `color` 선 색, `fill` 채우기(투명도 포함), `width` 선 굵기 |
| `path(key, [지점...], ...)` | 지면의 꺾은선 | `color`, `width`, `arrow` 끝 화살표. 지점은 좌표나 유닛 |
## HTTP(모든 언어)
Farsight 백엔드(로컬에서만 수신 대기):
```http
POST /api/instances/20/canvas
{"set": [
{"key": "banner", "kind": "text", "text": "HTTP에서 그린 캔버스", "frac": [0.5, 0.12], "center": true,
"color": "#FFDC50", "bg": [0, 0, 0, 180]},
{"key": "hp", "kind": "bar", "value": 0.8, "unit": 596125988, "lift": 260, "text": "80%"},
{"key": "zone", "kind": "circle", "center": 596125988, "radius": 600, "color": [255, 200, 0], "width": 4},
{"key": "route", "kind": "path", "points": [[-4587, -9092], [-5387, -8792]], "color": "#50C8FF"}
],
"remove": ["old"], "clear": false}
GET /api/instances/20/canvas 지금 그려진 요소 목록 + 그린 프레임 수
```
`kind`는 Python의 메서드 이름이고, 인자 이름도 같습니다. 유닛은 스냅샷의 주소 `addr`로 씁니다.
## 공유 메모리에 직접 쓰기
Python과 Farsight를 거치지 않을 수도 있습니다. 먼저 시맨틱 명령 `canvas_enable`(W3P 연산 코드 73)을 한 번 보내면, 런타임이 공유 메모리 블록 `Local\War3Canvas_`를 만듭니다. 구성은 헤더 64바이트 + 항목 256개 × 112바이트 + 64 KB 텍스트 / 지점 풀입니다. seqlock 방식으로 씁니다(시퀀스 번호를 홀수로 → 항목과 풀 쓰기 → 시퀀스 번호를 짝수로). 런타임은 매 프레임 한 번 읽고, 반쯤 쓰인 상태를 읽으면 이전 프레임 내용을 그대로 쓰며, 그린 프레임 수, 요소 수, 예외 횟수를 되써 줍니다. Python 참조 구현은 `sdk/python/w3canvas.py`이며, 구조체 정의는 프로토콜 헤더 파일에 있습니다. [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.
## 여러 프로그램이 동시에 그릴 때
모드, Farsight, MCP, 게이트웨이가 같은 게임에 동시에 그릴 수 있지만, 캔버스는 하나뿐입니다. 규칙은 **각 프로그램은 자기 요소만 건드린다**입니다.
- 쓰기 전에 이름 있는 락을 먼저 잡고, 기존 요소를 읽어 다른 프로그램의 것은 남기고 자기 것만 바꾼 뒤 다시 씁니다.
- 요소마다 누가 그렸는지(프로세스 ID + 프로세스 안 일련번호)를 기록합니다. 그린 프로그램이 종료되면 다음에 누군가 쓸 때 함께 정리되고, 그 프로그램의 버튼도 더 이상 클릭을 가로채지 않습니다.
- 요소 번호는 공용 카운터에서 할당하므로 겹치지 않습니다.
Python SDK는 이미 이렇게 동작하며, `clear()`도 자기 것만 지웁니다. 공유 메모리에 직접 쓴다면 이 규칙을 따르세요. 그러지 않으면 다른 프로그램의 것을 덮어써 버립니다. 레이아웃 세부 사항은 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.
## 실측과 주의 사항
- 2026-09-25 실측(1920×1080, 2배속): 요소 9개에 프레임당 0.27 ~ 0.34 ms, 약 63프레임/초, 예외 0회. 9개를 쓰는 데 6 ms. 영웅이 움직일 때 유닛을 따라가는 원, 텍스트, 체력 바가 모두 잘 따라갑니다. 내용이 바뀔 때만 텍스처를 다시 그리고, 위치만 옮길 때는 다시 그리지 않습니다.
- 게임 UI를 그린 뒤, 마우스 포인터를 그리기 전에 그립니다. 게임 자체의 체력 바, 유닛, UI 위를 덮고, 마우스 포인터는 그 위에 그려집니다. 하단 조작 패널과 상단 낮밤 시계는 피하지만, **맵 자체의 패널(오른쪽 위의 리더보드, 카운트다운)은 피하지 않습니다** — 직접 만든 패널은 오른쪽 위에 두지 마세요.
- 게임 중이 아닐 때(메인 메뉴, 결과 화면)는 월드 좌표와 유닛에 붙은 요소는 그리지 않고, 화면 위치에 둔 요소는 그대로 그립니다.
- 지면의 원은 원주 위 64개 점을 각각 지면에 투영한 것이라, 지형에 높낮이가 있으면 모양도 따라 굴곡집니다 — 이것이 맞습니다. 실제 지면 위에 그리기 때문입니다.
- 처음 열 때 훅 설치와 글꼴 워밍업에 약 1초가 걸리며, 그동안 텍스트 계열 요소는 그리지 않고 원과 선은 그대로 그립니다.
- 그리는 중 예외가 한 번이라도 나면 이번 세션에서는 더 이상 그리지 않습니다(머리 위 말풍선과 같은 보호 장치). `stats()`의 `faults`가 1이 됩니다.
- 텍스트, 이미지 경로, 지점을 합쳐 64 KB, 요소는 최대 256개입니다. 이미지 경로는 게임 프로세스가 읽을 수 있는 로컬 경로여야 합니다.
[AI 동료](https://war3ai.com/ko/docs/companion/)의 상태 패널도 캔버스로 그린 것입니다. 체력 바, 지금 하는 일, 기분, 처치 수와 치유 횟수를 보여 줍니다.
---
# UI와 입력
> 캔버스 위의 버튼과 선택 카드를 클릭할 수 있고, 마우스를 올리면 자동으로 강조됩니다. 단축키를 등록하고, 지면을 클릭해 위치를 고르고, 마우스가 가리키는 곳을 읽고, 로컬 플레이어가 무엇을 선택했는지 알 수 있습니다. 클릭, 단축키, 스킬 시전, 채팅 전문, 플레이어 퇴장이 모두 이벤트 스트림에 들어옵니다.
[캔버스](https://war3ai.com/ko/docs/canvas/)로 그린 것을 이제 **클릭할 수 있습니다**. 런타임이 게임 창의 입력을 넘겨받으므로, 외부 프로그램은 다음을 할 수 있습니다.
| 기능 | 한 줄 설명 | 게임에 전달되는가 |
|---|---|---|
| **클릭 가능한 캔버스 항목** | 버튼, 선택 카드, 패널: 클릭하면 `ui.click` 발생, 마우스를 올리면 자동 강조 | 버튼 위를 누른 클릭은 **전달되지 않음** |
| **단축키** | `F5`, `ctrl+shift+Q` 같은 조합을 등록하면, 누를 때 `hotkey` 발생 | 가로챌지 선택 가능(그 키가 만들어 내는 문자까지 포함) |
| **지면 클릭** | 월드를 클릭하면 지면 좌표와 함께 `mouse.world` 발생 | 가로챌지 선택 가능("위치를 클릭해 탑 짓기") |
| **마우스 위치** | 매 프레임 갱신: 화면 픽셀, 가리키는 지면 지점, 마우스가 올라가 있는 캔버스 항목 | — |
| **선택** | 로컬 플레이어가 무엇을 선택했는지. 바뀌면 바로 `selection.changed` 발생 | — |
모두 **로컬 입력 + 로컬 드로잉**입니다. 명령 스트림에 들어가지 않으므로 멀티플레이 게임에서도 안전합니다. 다만 콜백에서 월드를 바꾸면(유닛 생성, 속성 변경) 여전히 싱글플레이 게임에서만 쓸 수 있습니다.
## Python: g.ui
```python
ui = g.ui # 처음 쓸 때 런타임이 창 입력을 넘겨받음
ui.button("shop", "물약 사기(금 50)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("레벨 업! 보상을 하나 고르세요", [("힘 +5", "더 튼튼하게"), ("공격 속도 +20%", "더 강하게"), ("늑대 소환", "도우미 하나 추가")],
pause=True, on_pick=lambda g, i: give(g, i)) # 화면 가운데에 카드 한 줄. pause=True면 고르는 동안 게임 일시 정지
i = c.wait(timeout=30) # 블로킹으로 기다릴 수도 있음(그동안에도 이벤트를 계속 처리하므로 유실 없음)
ui.hotkey("F5", lambda g, ev: g.say(hero, "알겠어요!")) # 기본값은 가로챔
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right")) # 지면 클릭 잡기: 왼쪽·오른쪽 버튼 모두 알리고 가로챔
xy = ui.pick_point("지면을 클릭하세요: 탑을 어디에 지을까요?") # 블로킹 버전: 다음 왼쪽 버튼 지면 클릭 -> (x, y). Esc나 시간 초과 -> None
ui.cursor() # {'screen': (x, y), 'world': (x, y, z) 또는 None, 'hover': 'shop'}
ui.toast("3번째 웨이브가 옵니다!", seconds=3)
ui.close() # 자기 컨트롤과 단축키를 치움. 입력을 쓰는 다른 프로그램이 없을 때만 창 입력을 돌려줌
g.close() # 또는 연결 전체를 끊음(with Game(...) as g:로 써도 됨)
```
콜백의 인자는 `(g, ev)`이며, `g.events()`를 호출할 때 실행됩니다 — Bot과 [게임플레이 모드](https://war3ai.com/ko/docs/mods/)의 러너는 매 틱 이를 호출합니다. 콜백을 지정하지 않은 클릭은 `ui.clicks`에 쌓입니다. 콜백에서 예외가 나도 로그만 남기며, 다른 콜백과 이벤트에는 영향을 주지 않습니다.
캔버스를 직접 써도 됩니다: `g.canvas.text(..., clickable=True, hover=색)`. 클릭은 이벤트 스트림에서 받으며, `ev.key`는 그릴 때 지정한 key입니다. 클릭 가능한 요소를 그리면 입력이 자동으로 켜지므로, `g.ui`를 먼저 건드릴 필요가 없습니다.
**단축키 표기**: `F1` ~ `F24`, `A` ~ `Z`, `0` ~ `9`, `numpad0` ~ `numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`. 앞에 `ctrl+`, `shift+`, `alt+`를 붙일 수 있습니다.
> **주의**
>
> 보조 키 없는 문자와 숫자는 채팅 입력, 게임 단축키와 충돌합니다. F5 ~ F8처럼 게임이 쓰지 않는 키나 조합 키를 우선 쓰세요.
## 새 이벤트
`g.events()`에 다음이 추가되었습니다(전체 필드는 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/) 참고).
| kind | 언제 | 편의 필드 |
|---|---|---|
| `ui.click` | 상호작용 가능한 캔버스 항목이 클릭됨 | `.key` 캔버스 key, `.button`(`'left'` / `'right'`), `.mods` 보조 키 |
| `ui.hover` | 마우스가 캔버스 항목에 들어가거나 나감 | `.key`(나갈 때는 `None`) |
| `hotkey` | 등록한 단축키가 눌림 | `.key` 단축키 표기, `.mods` |
| `mouse.world` | 지면 클릭을 켰을 때, 월드를 클릭함 | `.x .y` 지면 좌표, `.button`, `.value`(1 = 가로챔) |
| `selection.changed` | 로컬 플레이어의 선택이 바뀜 | 유닛은 `g.selection()`으로 가져옴 |
| `spell.cast` | 유닛이 스킬을 시전함(스킬 쿨다운 시작) | `.spell` 4자 코드, `.b` 레벨, `.value` 쿨다운 초, `.x .y` 시전 지점 |
| `message` | 화면 메시지 영역에 한 줄이 표시됨 | `.text` 전체 텍스트, `.frame` 어느 영역인지, `.chat`(채팅일 때) |
| `player.left` | 플레이어가 나가거나 패배 판정으로 제거됨 | `.player` |
| `game.ended` | 게임에서 나감 | — |
## 채팅과 화면 메시지
플레이어가 채팅창에 입력한 내용은 `message` 이벤트의 `.chat`에서 바로 읽습니다.
```python
for ev in g.events():
if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
... # ev.chat = {'channel': '모두', 'sender': '플레이어 이름', 'text': '-follow'}
```
`g.messages()`는 별도의 커서를 가지며, 게임 안내("농장이 더 필요합니다", "그곳에는 건설할 수 없습니다")도 여기에 들어 있습니다. Bot을 작성할 때 이것을 쓰면 명령이 왜 실패했는지 알 수 있습니다.
## 다른 언어에서 쓰기
- **게이트웨이**: `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse`, `ui.cursor` 같은 메서드를 [게이트웨이](https://war3ai.com/ko/docs/gateway/)에서 같은 이름으로 쓸 수 있습니다. 원격에서는 콜백 함수를 넘길 수 없으므로, 클릭과 단축키는 이벤트 푸시로 받습니다(`ui.click` 이벤트에 `key`가 들어 있음).
- **공유 메모리에 직접 쓰기**: 먼저 시맨틱 명령 `input_enable`(W3P 연산 코드 74)을 보내면 런타임이 입력을 넘겨받기 시작합니다. 입력 블록 `Local\War3Input_`에 클라이언트가 단축키 표와 마우스 스위치를 쓰고, 런타임은 마우스 위치, 가리키는 지면 지점, 마우스가 올라가 있는 항목을 되씁니다. 캔버스 항목의 플래그 비트 `0x40`은 "상호작용 가능"을 뜻합니다. 레이아웃은 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.
## 여러 프로그램이 동시에 쓸 때
모드, Farsight, MCP, 게이트웨이의 각 세션이 한 게임에 동시에 버튼을 놓고 단축키를 등록해도 서로 간섭하지 않습니다.
- 각 프로그램은 자기 단축키와 지면 클릭 스위치를 등록하고, SDK가 모두의 것을 표 하나로 합쳐 런타임에 넘깁니다. 같은 키는 한 줄만 남기며, 이벤트는 모두에게 보내고 각자 키로 자기 단축키를 알아봅니다.
- `ui.close()`는 자기 것만 거둬들이며, 마지막 프로그램이 떠나야 창 입력을 돌려줍니다.
- 프로그램이 강제로 종료되어 뒷정리를 못 한 경우: 런타임이 2초마다 확인해, 등록한 프로그램이 모두 종료되었으면 그들이 남긴 단축키와 지면 클릭 가로채기를 지우고, 그들이 그린 버튼도 더 이상 클릭을 가로채지 않습니다.
## 실측
2026-09-25, 테스트 인스턴스에서 실게임 확인 16/16:
- 버튼 클릭 → `ui.click` + 콜백, 런타임의 가로채기 카운트 +1(게임은 이 클릭을 받지 못함). 버튼 바깥 클릭은 발생하지 않음
- F6 → `hotkey`. 지면 클릭 → `mouse.world`(가로챔)
- 팔라딘 하나를 생성해 선택 → `selection.changed`, `g.selection()`과 일치. 신성한 보호막 시전 → `spell.cast('AHds', 1, 35.0)`
- 맵 텍스트 → `message`. 채팅 → `message`, `.chat`에서 발신자와 내용을 파싱
- 컴퓨터 패배 처리 → `player.left`. 게임 종료 → `game.ended`
사람이 직접 마우스로 버튼을 클릭하는 것과 마우스를 올렸을 때의 강조도 하나씩 눈으로 확인했습니다.
## 한계와 주의 사항
- **위치는 실제 마우스 기준입니다**: 게임은 시스템 커서로 위치를 읽으므로, 호버와 `cursor()`는 실제 마우스를 반영합니다. 가로채기는 누르는 입력(클릭과 키)에만 적용됩니다.
- **마우스 포인터 아래에 그립니다**: 워크래프트는 포인터를 화면의 일부로 매 프레임 그려 넣습니다. 캔버스와 머리 위 말풍선은 모두 게임이 포인터를 그리는 단계 직전에 그리므로, 게임 UI는 덮고 포인터에는 덮입니다. 해당 프레임에 포인터를 그리지 않을 때(숨겨졌거나 컷신)만 마지막 단계에서 그립니다.
- **시스템 배율**: 직접 테스트를 작성해 창 메시지로 클릭을 보낼 때, DPI를 인식하지 않는 프로세스가 보낸 좌표는 시스템이 확대합니다(150% 배율에서 실측 ×1.5). 테스트 프로그램은 먼저 DPI 인식을 선언하세요. 사람이 직접 클릭하는 경우에는 영향이 없습니다.
- **처음에는 글꼴 워밍업**이 필요하며 약 1초가 걸립니다. 그동안에는 버튼이 아직 그려지지 않아 클릭할 수 없습니다.
- **게임 중이 아닐 때는 지면 클릭을 알리지 않습니다**: 메인 메뉴와 결과 화면에서는 `mouse.world`를 보내지도, 가로채지도 않습니다.
- 눌렀을 때 가로챈 클릭이라도, 떼기 전에 다른 프로그램으로 전환하거나 마우스를 창 밖으로 끌고 나가면 상태가 초기화되므로, 다음 떼기까지 가로채는 일은 없습니다.
- 1.27에는 새 게임 UI 프레임을 만드는 함수가 없습니다(1.31부터 있음). 여기의 버튼과 카드는 모두 런타임이 그린 것이라 스타일은 자유롭지만, 게임 자체의 메뉴 계층에는 나타나지 않습니다.
---
# JASS 채널
> 맵 제작자가 쓸 수 있는 JASS 함수 1291개를 이제 게임 밖에서 이름으로 바로 호출할 수 있습니다. 유닛 생성, 속성 변경, 특수 효과, 패널, 대화 상자, 사운드, 카메라, 전장의 안개…… Farsight 콘솔, 명령줄, HTTP, Python 네 가지 방법으로 쓸 수 있습니다.
맵 제작자가 맵 스크립트에서 쓸 수 있는 **JASS native 1291개**를 이제 모두 게임 밖에서 이름으로 바로 호출할 수 있습니다. 유닛 생성, 속성 변경, 특수 효과 그리기, 패널과 대화 상자 띄우기, 사운드 재생, 카메라 이동, 안개 변경…… 게임을 한층 더 커스터마이즈하는 데 쓰세요 — RPG 보조, [AI 동료](https://war3ai.com/ko/docs/companion/), 직접 만든 미니 게임, 디버깅 도구.
| 사용법 | 적합한 경우 | 진입점 |
|---|---|---|
| **Farsight "JASS 콘솔" 페이지** | 직접 시험해 보기, 보면서 고치기 | 왼쪽 사이드바 "시스템 → JASS 콘솔": 스크립트를 쓰고 실행을 누름. 오른쪽에서 분류별로 함수를 찾고, 클릭하면 스크립트에 삽입 |
| **명령줄** | 직접 시험해 보기, 또는 스크립트 파일로 만들어 반복 실행 | `python -m openwar3 jass --inst 20`(대화형), `-e "코드"`, `my_script.j`, `--list 키워드` |
| **HTTP** | 어떤 언어로 만든 외부 프로그램이든 | `POST /api/instances/{n}/jass` 등(아래 참고). Farsight 백엔드는 로컬에서만 수신 대기 |
| **Python** | 스킴, 동료, 도구 작성 | `g.jass.함수이름(...)`. 자주 쓰는 화면·상호작용 기능은 `openwar3.visual`에 묶여 있음 |
> **주의**
>
> 경계는 세 가지이며, 모두 메커니즘상 정해진 것입니다.
>
> - **싱글플레이 게임**(이 컴퓨터에서 컴퓨터 상대)에서만 월드를 바꿀 수 있습니다. 이 컴퓨터가 일방적으로 오브젝트를 만들거나 유닛을 바꾸면 멀티플레이 게임에서 다른 플레이어와 동기화가 깨집니다 — 멀티플레이 게임에서는 읽기 전용 함수(`Get*`, `Is*`, `Count*`……)만 허용합니다.
> - 이 컴퓨터의 자체 도구 전용입니다. 플레이어 신분으로 연결(`Game(player=N)`)했거나 공정 모드에서 호출하면 거부됩니다.
> - 싱글플레이 또는 LAN에서 직접 만든 게임에서만 사용하세요.
>
> 멀티플레이 게임에서 화면에 무언가를 더하려면 [캔버스](https://war3ai.com/ko/docs/canvas/)를 쓰세요. 런타임이 직접 그리며 게임 상태를 바꾸지 않습니다.
## 스크립트 작성법
콘솔, 명령줄, HTTP는 같은 스크립트를 씁니다. 한 줄에 한 문장이며, **JASS를 그대로 붙여 넣을 수도 있고**(`call` / `set` / `local`, `true` / `false` / `null`, `'Hpal'` 4자 코드, `//` 주석) Python 식으로 쓸 수도 있습니다.
```text
set h = hero() // 내장: 아군 주 영웅
local texttag t = CreateTextTag()
call SetTextTagText(t, "|cffffcc00+128 치명타!|r", 0.024)
call SetTextTagPosUnit(t, h, 60)
call SetTextTagVelocity(t, 0, 0.03)
call SetTextTagPermanent(t, false)
call SetTextTagLifespan(t, 4)
call SetTextTagVisibility(t, true)
call PingMinimapEx(h.x, h.y + 300, 3, 255, 0, 0, false)
set u = CreateUnit(Player(0), 'hfoo', h.x + 200, h.y, 270)
print("생성함", u, "영웅 레벨", GetHeroLevel(h))
```
- **변수는 계속 기억됩니다**: 같은 인스턴스, 같은 게임 안에서는 이번 블록에서 `set`한 변수를 다음 블록에서 이어서 쓸 수 있습니다. 게임이 바뀌면 자동으로 비워지며, 수동으로 비울 수도 있습니다.
- **내장 함수**: `hero()` 아군 주 영웅, `me()` 로컬 플레이어, `unit('hfoo')` 유닛 하나 찾기, `unit_at(x, y)`, `wait(초)`, `print(...)`. 유닛에서는 `.x`, `.y`, `.hp`, `.hp_max`, `.mana`, `.type`, `.owner`, `.level`을 읽을 수 있고, 사칙연산과 비교를 지원합니다.
- `if`, `loop`, `function`은 **지원하지 않습니다** — 로직을 쓰려면 Python의 `g.jass`(평범한 함수 호출입니다)를 쓰거나 [스킴](https://war3ai.com/ko/docs/schemes/)으로 만드세요.
- 오류가 나면 몇 번째 줄에서 왜 났는지 알려 줍니다(그런 함수가 없음, 인자 개수가 틀림, 정의되지 않은 변수……). 오류 이전의 문장은 이미 적용되어 있습니다.
인자와 반환값:
| 시그니처 | 전달할 값 | 설명 |
|---|---|---|
| 정수 | 숫자. `'Hpal'` 4자 코드는 자동 변환 | |
| 실수 | 숫자 | 런타임이 엔진이 요구하는 형식으로 변환 |
| 불리언 | `true` / `false` | |
| 문자열 | `"..."` | 중국어 등 비ASCII 문자와 게임 색상 코드 지원. 게임이 보관하는 문자열(떠다니는 텍스트, 패널, 버튼, 채팅 명령)은 모두 그 자리에서 복사해 두므로 안전 |
| 핸들 | 변수에 담긴 핸들, 또는 유닛(`hero()` 같은 것은 자동으로 핸들로 변환) | |
| 함수(code) | `null`만 가능 | 밖에서는 JASS 함수를 넘길 수 없음. `TimerStart(t, 60, false, null)` 같은 것은 가능 |
| 문자열 반환 | — | 엔진이 반환하는 것은 문자열 테이블 번호라서 텍스트를 읽어 올 수 없음. 유닛 이름은 `g.map_data.name_of` 사용 |
## 분류
함수는 이름으로 분류되어 있으며, 콘솔 오른쪽과 `--list` 모두 이 분류를 따릅니다.
| 분류 | 개수 | 예 |
|---|---|---|
| 화면 효과 | 80 | 떠다니는 텍스트, 번개 연결선, 특수 효과, 지면 이미지, 지면 표식, 유닛 색 변경 / 크기 조절 / 애니메이션 재생 |
| UI 패널 | 146 | 멀티보드, 리더보드, 타이머 창, 대화 상자, 퀘스트, 화면 텍스트, 미니맵 핑, 초상화 대사, 전체 화면 필터 |
| 카메라 | 44 | 카메라 필드, 팬 이동, 카메라 흔들기 |
| 사운드·음악 | 50 | 사운드 생성과 재생, 음악 재생 |
| 안개·시야 | 25 | 가시 영역, 안개 켜기/끄기 |
| 아이템 / 영웅 / 유닛 | 63 / 32 / 161 | 아이템 생성, 영웅 레벨 설정, 소유자 변경, 스킬 추가 |
| 플레이어 / 동맹 / 자원 | 71 | 동맹 설정, 금·목재 변경 |
| 트리거 / 이벤트 / 타이머 | 62 | 트리거 생성, 이벤트 등록, 타이머 |
| 지형 / 날씨 / 파괴 가능 오브젝트 | 45 | 날씨 효과, 지형 변경, 파괴 가능 오브젝트 생성 |
| 게임 진행 | 57 | 게임 속도, 일시정지, 낮밤 시각 |
| 기타 | …… | 유닛 그룹과 영역, 저장소, 컴퓨터 AI 스크립트, 형 변환과 수학, 이벤트 응답…… |
2026-09-24에 하나씩 실제 게임에서 호출해 효과를 눈으로 확인한 것은 **94개**입니다. 나머지도 같은 경로를 거치지만, 하나하나 효과를 확인하지는 않았습니다.
> **참고**
>
> "이벤트 응답" 계열 함수(`GetTriggerUnit`, `GetClickedButton`……)는 트리거가 실행되는 그 순간에만 값이 있어서, 밖에서 호출하면 0이나 빈 값을 받습니다. "일어났는지 여부"를 알고 싶다면 아래의 이벤트 카운트를 쓰세요.
## HTTP
Farsight 백엔드(기본 `127.0.0.1:8866`, 로컬에서만 수신 대기):
```http
GET /api/jass/natives?q=TextTag&cat=visual
POST /api/instances/20/jass {"code": "set h = hero()\ncall PingMinimapEx(h.x, h.y, 3, 255, 0, 0, false)"}
-> {"ok": true, "rows": [...], "printed": [...], "vars": {...}}
-> 오류: {"ok": false, "error": "第 2 行:...", "line": 2} ("第 2 行" = 2번째 줄)
POST /api/instances/20/jass/call {"name": "SetUnitScale", "args": [{"unit": 599669636}, 1.4, 1.4, 1.4]}
POST /api/instances/20/jass/reset 기억된 변수 지우기
```
유닛 인자는 `{"unit": 주소}`로 쓰며, 주소는 스냅샷에 있는 유닛의 `addr`입니다. 실측 요청 1회에 60 ~ 90 ms.
## Python: g.jass와 openwar3.visual
```python
j = g.jass
t = j.CreateTextTag()
j.SetTextTagText(t, "안녕", 0.024) # 인자 규칙은 스크립트와 같음. 스냅샷의 유닛, 아이템 객체를 그대로 넘겨도 됨
j.signature("CreateImage") # 시그니처 조회
```
`openwar3.visual.Visual(g)`는 실측으로 확인한 자주 쓰는 화면 효과를 한 줄에 하나씩 쓸 수 있게 묶어 둔 것입니다(틱마다 `v.tick()`을 한 번 호출하면 만료된 것은 지우고, 유닛을 따라가는 선과 원은 옮겨 줌. `v.clear()`는 전부 삭제).
| 메서드 | 효과 |
|---|---|
| `float_text(텍스트, 유닛 또는 지점, ...)` | 떠다니는 텍스트: 피해 숫자, 머리 위 알림. 비ASCII 문자와 색상 모두 가능 |
| `link(a, b, kind)` | 두 유닛 사이의 선, 유닛을 따라감: 견인 / 영혼 연결 / 생명력 흡수 / 치유의 물결 |
| `effect(모델, 유닛 또는 지점, ...)` | 특수 효과 모델: 머리 위, 발밑, 또는 한 번 재생(폭발, 빛기둥) |
| `ring(유닛 또는 지점, 반지름, color)` | 지면의 범위 원: 스킬 범위, 위험 지역, 집결 지점. 유닛을 따라가게 할 수 있음 |
| `ping(지점, color)` | 미니맵 핑 |
| `board(제목, 줄...)` | 오른쪽 위 멀티보드(아이콘 포함). 칸 단위로 수정 가능 |
| `countdown(제목, 초)` | 오른쪽 위 타이머 창. 게임이 알아서 초를 셈 |
| `scene(이름, 대사, portrait)` | 초상화 대사: 하단 초상화가 말하는 유닛으로 바뀌고, 화면에 "이름: 대사" 자막이 나옴 |
| `screen_tint(color, alpha)` | 전체 화면 필터(기본값은 가장자리가 붉게 물듦: 체력 경고) |
| `sound(경로)` / `reveal(지점, 반지름, 초)` / `look(유닛, ...)` | 사운드 재생 / 안개 일부 걷기 / 유닛 색 변경, 확대, 애니메이션 재생, 번쩍임 |
## 상호작용: JASS 함수를 쓰지 않고도 플레이어가 한 일 알아내기
JASS에서 플레이어에게 반응하려면 트리거 함수를 써야 하는데, 밖에서는 함수를 넘겨줄 수 없습니다. 해법은 이렇습니다. **조건도 동작도 없는 빈 트리거를 만들어 이벤트만 등록하고, 그 트리거가 몇 번 실행됐는지 셉니다.** 실측 결과 빈 트리거도 실행 횟수가 그대로 집계됩니다.
| 메서드 | 용도 |
|---|---|
| `chat_commands(["-follow", "-stay"])` → `.poll()` | 플레이어가 채팅창에 입력한 명령(정확히 일치, 또는 앞부분 일치) |
| `menu(제목, [버튼...])` → `.clicked()` | 화면 가운데 버튼 메뉴에서 어느 것을 눌렀는지 |
| `hotkeys(("left", "right", "up", "down", "esc"))` → `.poll()` | 방향키, Esc가 몇 번 눌렸는지 |
| `on("TriggerRegister...Event", 인자...)` → `.poll()` | 임의의 JASS 이벤트가 몇 번 발생했는지: 유닛 사망, 영역 진입, 피해, 타이머…… |
한계는 "몇 번 일어났는지"만 알 뿐 "누가, 무슨 글자를 입력했는지"는 모른다는 것입니다. 누구인지 구분하려면 대상마다 카운터를 따로 만드세요. [AI 동료](https://war3ai.com/ko/docs/companion/)의 채팅 명령도 이렇게 연결했습니다.
## 주의
- **만든 것은 직접 지워야 합니다**: 떠다니는 텍스트, 연결선, 이미지, 패널, 트리거…… 지우지 않으면 계속 남습니다(`Visual.clear()`는 자신이 만든 것을 지웁니다). 게임에는 떠다니는 텍스트를 동시에 최대 약 100개까지 둘 수 있습니다.
- **BJ 함수는 native가 아닙니다**: `CreateTextTagUnitBJ` 같은 함수는 맵 스크립트에서 native를 조합해 만든 것이라 여기에는 없습니다 — 그 구현을 보고 native를 호출하세요.
- **어떤 상수는 먼저 변환해야 합니다**: 예를 들어 `ConvertPlayerColor(1)`, `ConvertFogState(4)`(값은 common.j 참고).
- 호출 1회에 약 13 ms(핸들 변환 포함). 프로토콜 계층은 W3P 연산 코드 70 ~ 72이며, [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)을 참고하세요.
---
# 게임플레이 모드
> 스킴은 여러분 대신 싸우는 AI일 뿐 아니라 하나의 규칙이 될 수도 있습니다. 여러분은 게임 창에서 직접 플레이하고, 모드는 시작 배치, 몬스터 생성, 보상, 화면의 버튼과 선택 카드, 승패 판정을 맡습니다. openwar3.Mod를 상속하면 파일 하나가 곧 하나의 게임 방식입니다.
[AI 스킴](https://war3ai.com/ko/docs/schemes/)에는 두 종류가 있습니다. `kind: bot`은 여러분 대신 싸우는 AI이고, `kind: mod`는 **하나의 규칙**입니다 — 여러분이 게임 창에서 직접 플레이하고, 모드가 과제를 냅니다. 시작 배치를 어떻게 할지, 시간이나 이벤트에 따라 몬스터를 생성할지, 어떤 보상을 줄지, 화면에 어떤 버튼과 선택 카드를 보여 줄지, 언제 승리로 칠지를 정합니다.
모드는 이미 있는 기능만으로 만들어집니다. [UI와 입력](https://war3ai.com/ko/docs/ui-input/)(클릭할 수 있는 버튼, 카드, 단축키, 지면 클릭), [캔버스](https://war3ai.com/ko/docs/canvas/)(패널, 진행 바, 경로), [JASS 채널](https://war3ai.com/ko/docs/jass/)(유닛 생성, 속성 변경, 아이템 지급), 이벤트 스트림(사망, 레벨 업, 스킬 시전, 채팅).
## 예제 두 개
Farsight의 "AI 스킴" → "내장"에서 바로 고를 수 있습니다.
| 모드 | 게임 방식 | 사용하는 기능 |
|---|---|---|
| **영웅 로그라이크** `builtin/hero-roguelike` | 여러분에게는 팔라딘 하나뿐이고, 몬스터가 웨이브마다 사방에서 몰려옵니다. 레벨이 오를 때마다 화면 가운데에서 강화 셋 중 하나를 고릅니다(고르는 동안 게임 일시 정지). 10웨이브를 버티면 승리, 영웅이 죽으면 패배 | `g.ui.choice`(클릭 가능한 카드 + 일시 정지), `hero.levelup` / `killed` / `spell.cast` 이벤트, 채팅 `-help`, JASS로 영웅 속성 변경, 아이템 지급 |
| **무한 디펜스** `builtin/endless-defense` | 몬스터가 반대편 시작 지점에서 지면의 빨간 선을 따라 본진으로 돌진합니다. 한 웨이브를 막을 때마다 금을 받습니다. 화면 버튼을 클릭하거나 F7을 누르면 다음 웨이브를 앞당겨 부르고 보상은 ×1.5. F8을 누른 뒤 왼쪽 버튼으로 지면을 클릭하면 무료 화살탑을 하나 세웁니다(오른쪽 버튼으로 취소) | `g.ui.button`, `g.ui.hotkey`, `g.ui.mouse`(지면 클릭 잡기), 캔버스 패널 / 진행 바 / 경로, JASS로 몬스터 생성과 금 지급 |
두 예제는 각각 150줄 정도이며, 코드는 `brains/examples/mod_hero_roguelike.py`와 `brains/examples/mod_endless_defense.py`에 있습니다.
```bash
python tools/play.py --bot brains/examples/mod_hero_roguelike.py --inst 9 # 게임을 시작하면 모드가 넘겨받고, 여러분은 게임 창에서 플레이
```
## 모드 작성하기
```python
from openwar3 import Mod
class Survive(Mod):
name = "survive"
def on_start(self, g):
super().on_start(g) # 싱글플레이 확인 + 컴퓨터 상대 억제
self.foe = self.wave_player(g) # 빈 슬롯 플레이어 하나를 "웨이브 플레이어"로: 누구와도 동맹하지 않고 컴퓨터 AI도 없음
self.every(30, self.wave) # 30게임 초마다 한 웨이브(일시 정지 중에는 흐르지 않음)
g.ui.hotkey("F7", lambda g, ev: self.wave(g))
def wave(self, g):
self.spawn_ring(g, self.foe, "ugho", 6, self.home(g), 1400, attack_to=self.home(g))
def on_event(self, g, ev):
if ev.kind == "unit.died" and ev.type == "htow":
self.finish("loss", "본진이 무너졌습니다")
```
`Mod`는 `Bot`에 다음을 더합니다.
| 메서드 / 속성 | 설명 |
|---|---|
| `on_start / on_tick / on_event / on_end` | Bot과 같음. `on_start` / `on_tick`을 재정의할 때는 먼저 `super()`를 호출하세요 |
| `every(초, fn, first=)` / `after(초, fn)` | **게임 시간** 기준 타이머. 콜백은 `fn(g)` |
| `finish(result, reason)` | 이 게임을 끝냄(`'win'` / `'loss'` / `'unknown'`): 러너가 다음 틱에 멈추고, 화면 가운데에 결과 패널을 그리며, 스킴 전적은 이 값으로 기록 |
| `wave_player(g)` | 첫 번째 빈 슬롯 플레이어. 웨이브 플레이어로 사용 |
| `spawn_ring(g, 플레이어, 유닛, 개수, 중심, 반지름, attack_to=)` | 원 둘레에 유닛을 생성. 한 웨이브에 수십 개여도 끊기지 않음. JASS 핸들을 반환 |
| `alive_of(g, 플레이어)` / `attack_move_all(g, 플레이어, 지점)` | 어떤 플레이어의 살아 있는 유닛 / 모두 공격 이동(몇 초마다 호출하면 몬스터가 쫓아옴) |
| `home(g)` / `hud(g, 제목, 줄)` | 아군 본진의 위치 / 오른쪽 위 정보 패널 |
| `neutralize_ai = True` | 게임 시작 시 컴퓨터 상대를 억제: 그 유닛을 5초마다 한 번씩 멈추고 금과 목재를 0으로. 대전 맵에는 항상 컴퓨터가 하나 있으므로, 모드가 직접 규칙을 정할 때 방해하지 않게 함 |
| `single_player_only = True` | 다른 사람 플레이어가 있으면 실행을 거부(월드를 바꾸는 JASS는 다른 사람과 동기화를 깨뜨림) |
| `linger_s = 6` | 승패가 난 뒤 결과 화면에서 몇 초 머물고 종료 |
`finish()`는 `Bot`에서도 쓸 수 있습니다. 일반 Bot도 스스로 종료를 선언할 수 있습니다.
## 스킴으로 만들어 공유하기
`scheme.json`에 `"kind": "mod"`를 쓰고, 진입 파일에 `Mod`의 하위 클래스를 하나 정의합니다.
```json
{"id": "survive", "name": "10웨이브 버티기", "kind": "mod", "entry": "survive.py", "class": "Survive"}
```
모드는 항상 **공정 모드를 쓰지 않으며**(과제를 내는 심판이므로 맵 전체를 보고 월드를 바꿔야 함), **대전 규칙으로 승패를 판정하지도 않습니다**(승패는 `finish`로 보고). 매니페스트에 `fair` / `judge`를 써도 효과가 없습니다. zip 내보내기, 가져오기, 신뢰, 전적은 Bot 스킴과 완전히 같습니다. [AI 스킴](https://war3ai.com/ko/docs/schemes/)을 참고하세요. 모드도 코드이므로, 다른 사람의 모드는 처음 실행하기 전에 똑같이 신뢰를 확인해야 합니다.
## 실측
2026-09-25, 테스트 인스턴스에서:
- **영웅 로그라이크**: 첫 웨이브가 생성되고 오른쪽 위 패널이 갱신됨. 영웅을 3레벨로 올림 → 화면 가운데에 카드가 뜨고 게임 시계가 멈춤. 카드를 두 번 클릭 → 강화 두 번이 적용됨(힘 22 → 27), 시계가 다시 흐름.
- **무한 디펜스**: 패널, 지면의 경로, 버튼이 모두 표시됨. F8 + 지면 클릭 → 본진 옆에 방어탑이 하나 생김. 이번 웨이브를 다 처리하기 전에 버튼 클릭 → "이번 웨이브를 아직 다 처리하지 못했습니다" 안내.
## 한계
- **싱글플레이 게임 전용**: 유닛 생성과 속성 변경은 JASS 채널을 거치므로 멀티플레이 게임에서는 동기화가 깨집니다. 이는 락스텝 모델 때문이며, 멀티플레이용 게임 방식은 동기화 채널을 기다려야 합니다([로드맵](https://war3ai.com/ko/roadmap/) 참고).
- 모드는 맵 전체를 봅니다 — 과제를 내는 쪽이지 플레이어가 아니기 때문입니다.
- 대전 맵의 컴퓨터 상대는 "억제"될 뿐 제거되지 않습니다(제거하면 대전 규칙의 승리 판정이 발동합니다).
---
# Farsight 콘솔
> 로컬 웹 콘솔이자 유일한 진입점입니다. 게임 디렉터리 설정, 각 서비스 시작과 중지, 게임 인스턴스 시작과 중지, 다음 게임 설정, AI의 생각 보기, 수동 명령, 중계 연출, 대전 기록을 제공합니다.
Farsight는 여러분의 컴퓨터에서 실행되는 웹 콘솔이며, **127.0.0.1에서만 수신 대기합니다**. 시스템 전체의 유일한 진입점이기도 합니다. 게임 시작, AI 교체, 게이트웨이, 머리 위 말풍선, 로컬 LLM을 모두 여기서 조작하므로 다른 스크립트를 찾을 필요가 없습니다.
```bash
start.bat # 배포 확인 후 Farsight http://127.0.0.1:8866 열기
start.bat 5 6 # 5, 6번 인스턴스의 테스트도 함께 시작(게임 + 레퍼런스 브레인)
start.bat restart # Farsight 백엔드만 재시작(서버 코드를 고친 뒤 사용. 게임과 각 서비스는 영향 없음)
stop.bat # 모두 완전히 중지
```
포트는 `openwar3.json`의 `ports.console`에서 바꿉니다(기본값 8866).
## 컨트롤 센터
Farsight의 첫 화면입니다.
- **게임 디렉터리**: 자동으로 찾거나 직접 고르고, 게임 버전을 확인하고, 여러분의 게임에서 데이터를 추출합니다.
- **로컬 서비스**: [게이트웨이](https://war3ai.com/ko/docs/gateway/), [머리 위 채팅 말풍선](https://war3ai.com/ko/docs/speech/), 로컬 LLM(LM Studio), 웹사이트 로컬 미리보기. 카드마다 시작, 중지, 재시작, 로그 보기를 할 수 있습니다. [MCP](https://war3ai.com/ko/docs/mcp/) 서버가 클라이언트에 연결되어 있는지도 표시합니다.
- **환경 점검**: Python, 런타임 파일, 게임 데이터, AMAI 데이터 등 각 부분이 제대로 설치됐는지 보여 줍니다.
- **모두 중지**(오른쪽 위): 게임 인스턴스, AI, 게이트웨이, 말풍선, 이 시스템이 쓰는 로컬 모델, Farsight 백엔드를 차례로 모두 멈춥니다. `stop.bat`을 더블클릭하는 것과 같습니다. MCP 서버는 Claude 같은 클라이언트가 관리하므로 중지되지 않고, LM Studio 프로그램 자체도 닫히지 않습니다.
## 페이지
| 그룹 | 페이지 | 하는 일 |
|---|---|---|
| 총괄 | 컨트롤 센터 | 앞 절 참고 |
| 대전 | 개요 | 현재 인스턴스의 대전 현황 |
| | 전장 지휘 | 맵 뷰. 수동 명령 가능(수동 명령은 점유 테이블에서 우선순위가 가장 높음: 95) |
| | 유닛 데이터 | 유닛별 오더, 작업 대상, 마나, 영웅 레벨과 경험치, 스킬 쿨다운, 인벤토리 |
| | AI 결정 / 전투 결정 | 레퍼런스 브레인이 이번 틱에 무슨 생각을 하는지, 전투 결정 하나하나의 상세 내용 |
| | 운영 코치 | [LLM 참모](https://war3ai.com/ko/docs/llm-coach/)의 상태: 모델 서버가 떠 있는지, 인스턴스마다 연결됐는지, 가장 최근 제안과 그때 본 입력 |
| | 중계 데스크 | 자동 카메라 연출, 머리 위 체력 바 |
| | 머리 위 말풍선 | 유닛이 말하게 하기, 로컬 모델과 대화, 일꾼 수다 모임, 카메라 대화, 전황 트리거, 모델 설정. [머리 위 말풍선](https://war3ai.com/ko/docs/speech/) 참고 |
| | 명령 속도 | APM과 명령 처리량 |
| | 이벤트와 입력 | 이번 게임에서 일어난 일: 스킬 시전, 채팅과 화면 메시지, 버튼 클릭, 단축키, 지면 클릭, 선택, 플레이어 퇴장. 분류별로 필터링 가능. 옆에는 마우스 위치, 마우스를 올린 항목, 로컬 플레이어의 선택. [UI와 입력](https://war3ai.com/ko/docs/ui-input/) 참고 |
| 기록 | 로그 / 대전 기록 | 인스턴스별 로그 소스, 게임마다 결과, 경기 시간, 최대 병력 |
| | 이슈 메모 | 게임 중 Pause/Break를 눌러 일시 정지하고 시점을 기록한 뒤, 나중에 여기서 설명을 보충 |
| 시스템 | 인스턴스와 게임 시작 | 인스턴스 시작/중지. **다음 게임**의 맵(대전 맵, RPG / 커스텀 맵도 선택 가능), 양측 종족, 난이도, 배속 설정. 인스턴스마다 AI 스킴 하나를 선택. "테스트 시작"으로 게임 + AI를 한 번에 실행 |
| | AI 스킴 | 스킴 가져오기, 내보내기, 복사, 신뢰, 삭제. 인스턴스의 스킴 전환(진행 중인 게임도 즉시 다른 AI가 이어받게 할 수 있음), 스킴별 전적 확인. [AI 스킴](https://war3ai.com/ko/docs/schemes/) 참고 |
| | JASS 콘솔 | JASS 스크립트를 작성하고 실행. 오른쪽에서 함수 1291개를 분류별로 조회하고 클릭 한 번으로 스크립트에 삽입. 변수는 같은 게임 안에서 계속 유지됨. [JASS 채널](https://war3ai.com/ko/docs/jass/) 참고 |
| | 연결과 확장 | 게이트웨이 상태와 원클릭 시작. 개발 / 플레이어 / 관전자 역할별로 생성한 연결 주소, MCP 연결 명령과 설정. JS, Python 예제. [게이트웨이](https://war3ai.com/ko/docs/gateway/), [MCP](https://war3ai.com/ko/docs/mcp/) 참고 |
| | 데이터와 디스크 | 녹화, 대전 기록, 로그 등 실행 데이터가 각각 디스크를 얼마나 차지하는지, 최근 하루 동안 얼마나 늘었는지, 무엇을 지워도 되는지(Farsight는 아무것도 자동으로 삭제하지 않음) |
| | 설정 | 게임 디렉터리, 인터페이스 언어, 외관(다크 / 라이트, 모던 / 워크래프트 스타일) 등 |
| | 피드백과 제안 | 문제가 생기거나 제안이 있으면 바로 보냄. 진단 정보는 체크했을 때만 첨부하며, 보내기 전에 미리 볼 수 있음 |
Ctrl + K를 누르면 명령 팔레트가 열립니다. 페이지 이동, 인스턴스 전환, 현재 게임 종료, 새 브레인 시작을 할 수 있습니다.
사이드바 아래쪽의 "최근 업데이트"에는 Farsight와 플랫폼에 최근 추가된 내용이 나옵니다. Farsight는 시작할 때(그 뒤로는 6시간마다) War3AI.com에 새 버전이 있는지 확인하고, 있으면 알려 줍니다. Farsight가 업데이트되면 페이지 위쪽에 배너가 뜹니다. 입력 중인 내용을 저장한 다음 "새로 고침"을 누르세요.
## 다중 인스턴스
`runtime/farm.py`이 다중 실행을 맡습니다(Farsight가 인스턴스를 시작하고 중지할 때 대신 호출합니다). 원본 `War3.exe` 런처를 `War3-.exe`로 복사해 이름을 바꾸고(게임 파일은 전혀 수정하지 않음), 인스턴스마다 번호 하나와 디렉터리 하나(`bin/inst/`)를 둡니다. 한 게임이 끝나면 `next_game.json`(콘솔의 "인스턴스와 게임 시작" 페이지에서 바꾸는 파일)에 따라 다음 게임을 자동으로 시작합니다.
> **팁**
>
> 여러분의 Bot은 `--inst N`으로 지정한 인스턴스에 연결합니다. 콘솔의 "인스턴스와 게임 시작" 페이지에서 어떤 번호가 사용 중인지 볼 수 있으니, 레퍼런스 브레인과 번호가 겹치지 않게 하세요.
## 방송 페이지
`http://127.0.0.1:8866/live`는 OBS의 "브라우저 소스"에 넣기 좋은 스크롤 로그 페이지로, AI의 결정과 전황을 보여 줍니다.
## API
콘솔의 서버는 로컬 REST + WebSocket API 모음(인스턴스 상태, 다음 게임 설정, 유닛 상세, 수동 명령, 로그, 대전 기록, 중계 연출, AI 스킴, JASS 호출, 캔버스……)이며, 웹 페이지는 그 클라이언트 중 하나일 뿐이고 어떤 언어의 프로그램이든 직접 호출할 수 있습니다. API 목록은 `console/server/app.py`의 파일 헤더에 적혀 있습니다. 스킴, JASS, 캔버스 세 그룹의 사용법은 각각 [AI 스킴](https://war3ai.com/ko/docs/schemes/), [JASS 채널](https://war3ai.com/ko/docs/jass/), [캔버스](https://war3ai.com/ko/docs/canvas/)를 참고하세요.
---
# AI 스킴
> 스킴 하나가 곧 완전한 AI 하나입니다. Farsight에서 클릭 한 번으로 바꾸고, 진행 중인 게임도 즉시 새 AI가 넘겨받을 수 있습니다. zip으로 내보내 공유하고, 다른 사람의 스킴을 가져와 테스트하며, 스킴마다 전적이 자동으로 집계됩니다.
**스킴** 하나 = 완전한 AI 하나: 폴더 하나 + 매니페스트 `scheme.json` + 코드. 게임 인스턴스마다 스킴을 하나씩 고르며, Farsight에서 클릭 한 번으로 바꿀 수 있고 **진행 중인 게임도 즉시 새 스킴이 넘겨받습니다**.
다른 사람이 공유한 스킴을 가져오면 **별도의 영역**에 들어가므로 여러분의 스킴과 서로 영향을 주지 않습니다. 고치고 싶다면 "내 스킴으로 복사"하세요.
```text
schemes/
mine// 내 스킴: 직접 쓴 것, 또는 다른 스킴에서 복사해 고친 것(마음대로 수정, 다음 게임부터 적용)
installed// 설치됨: 다른 사람이 공유한 zip을 여기에 풂(처음 실행하기 전에 신뢰 확인 필요)
brains/xwar3/ 내장: 레퍼런스 브레인(완전한 AI)
brains/examples/ 내장: 교육용 예제 4개 hello / rush / macro / micro, 동료 예제 buddy, 게임플레이 모드 2개(영웅 로그라이크, 무한 디펜스)
```
스킴이 꼭 여러분 대신 싸우는 AI일 필요는 없습니다. `kind: mod`인 스킴은 하나의 **게임 규칙**입니다. 여러분이 직접 플레이하고 스킴은 과제를 냅니다. [게임플레이 모드](https://war3ai.com/ko/docs/mods/)를 참고하세요.
## Farsight에서 사용하기
"AI 스킴" 페이지(왼쪽 사이드바 "시스템 → AI 스킴"):
| 작업 | 하는 일 |
|---|---|
| 스킴 가져오기(zip) | `installed/`에 설치. 같은 id가 이미 설치되어 있으면 교체할지 물음(교체하면 신뢰를 다시 확인해야 함) |
| 인스턴스에 적용… | 인스턴스 선택 + "즉시 적용"(현재 AI를 멈추고 새 스킴이 이 게임을 넘겨받음) 또는 "다음 테스트 시작부터 적용" |
| 내 스킴으로 복사 | `mine/`에 복사본을 만들고, 작성자는 "나", 버전은 0.1.0으로 기록하며, 어느 스킴의 어느 버전에서 복사했는지도 기록 |
| zip 내보내기 | `-<버전>.zip`으로 묶음. 다른 사람에게 보내면 그것이 곧 공유 |
| 폴더 열기 | 탐색기에서 스킴 폴더를 열어 코드를 직접 수정 |
| 신뢰 | 다른 사람의 스킴은 처음 실행하기 전에 반드시 눌러야 함(아래 "신뢰와 보안" 참고) |
| 최근 전적 | 이 스킴의 게임별 승패, 경기 시간, 종료 사유 |
| 삭제 | "내 스킴"과 "설치됨"만 삭제 가능. 인스턴스가 사용 중이면 삭제 불가 |
인스턴스 카드에도 "AI 스킴" 줄이 추가되었습니다. 드롭다운에서 스킴을 고르고 → "전환(즉시 적용)". 인스턴스가 실행 중이 아닐 때는 버튼 이름이 "선택"이며, 다음에 "테스트 시작"을 누르면 그 스킴으로 AI를 시작합니다.
## 매니페스트 scheme.json
```json
{
"format": 1,
"id": "fast-rush",
"name": "3분 러시",
"version": "1.2.0",
"author": "홍길동",
"description": "이 AI가 어떤 전략을 쓰는지 한 문장으로 설명",
"entry": "rush_bot.py",
"class": "RushBot",
"fair": true,
"hz": 5,
"races": ["human", "orc"],
"license": "MIT"
}
```
| 필드 | 필수 | 설명 |
|---|---|---|
| `id` | ✔ | 영문 소문자, 숫자, `-`, `_`. 2 ~ 41자 |
| `entry` | ✔ | 스킴 폴더 안의 `.py` 파일 하나(절대 경로 불가, `..` 불가) |
| `kind` | | 기본값 `bot`(`openwar3.Bot`의 하위 클래스, 여러분 대신 싸움). `mod` = [게임플레이 모드](https://war3ai.com/ko/docs/mods/)(`openwar3.Mod`의 하위 클래스. 항상 공정 모드를 쓰지 않으며, 대전 규칙으로 승패를 판정하지 않음) |
| `class` | | 진입 파일 안의 Bot(또는 Mod) 하위 클래스 이름. 생략하면 진입 파일의 마지막 `openwar3.Bot` 하위 클래스를 사용 |
| `fair` | | 기본값 `true`: 시야 안의 것만 보며, 아레나와 같은 규칙. `false` = 맵 전체가 보이며, 이때만 [JASS 채널](https://war3ai.com/ko/docs/jass/)을 쓸 수 있음(동료에게 필요) |
| `judge` | | 기본값 `true`: 대전 규칙으로 승패 판정. RPG / 동료 스킴은 `false` |
| `hz` | | `on_tick`을 초당 몇 번 호출할지. 기본값 5 |
| `format` | | 매니페스트 형식 버전. 현재 1. 이 컴퓨터의 OpenWar3보다 새 버전이면 거부하고 업데이트를 안내 |
| 기타 | | `name`, `version`, `author`, `description`, `races`, `license`, `homepage`, `forked_from`은 표시용 |
스킴 폴더는 Python 모듈 검색 경로에 추가되므로, 진입 파일에서 같은 폴더의 다른 파일을 `import`할 수 있습니다. 서드파티 패키지(numpy, torch……)는 자동으로 설치되지 않습니다 — 무엇이 필요한지 `description`에 분명히 적으세요.
**가장 작은 스킴은 파일 두 개면 충분합니다**:
```python
# my_bot.py
from openwar3 import Bot
class MyBot(Bot):
def on_tick(self, g):
for w in g.idle_workers():
mine = g.nearest(g.gold_mines(), w)
if mine:
g.gather(w, mine)
```
```json
{"id": "my-first", "name": "나의 첫 AI", "entry": "my_bot.py"}
```
`schemes/mine/my-first/`에 넣고 Farsight를 새로 고치면 바로 보입니다. 더 간편한 출발점: "내장"에서 예제를 하나 골라 "내 스킴으로 복사"를 누르세요.
## 실행 방식과 전적
스킴은 **스킴 러너**가 실행합니다(Farsight의 "테스트 시작 / 전환"이 띄우는 것이 바로 이것입니다).
```bash
python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6
```
- 인스턴스마다 상주하는 감독 프로세스가 하나 있고, **게임마다 자식 프로세스를 하나 띄워** 스킴을 실행합니다. 스킴 코드가 죽어도 감독 프로세스는 영향을 받지 않으며, "내 스킴"의 코드를 고치면 다음 게임부터 자동으로 새 코드를 씁니다.
- 게임이 끝날 때마다 전적을 한 줄 기록합니다: 스킴, 버전, 작성자, 승패, 사유, 게임 시간, 오류 횟수. Farsight의 승률은 여기서 집계합니다.
승패 판정 방법:
| 상황 | 기록 |
|---|---|
| 상대 건물이 전부 없어짐 | 승 |
| 아군 건물이 전부 없어짐(병력이 살아 있어도 마찬가지 — 대전에서는 이렇게 패배를 판정함) | 패 |
| 아군 유닛이 전부 없어짐 | 패 |
| Farsight에서 수동으로 종료 / 중지 | 미정 |
| 전환 시점에 이 게임이 이미 60 게임 초 이상 진행됨(중간에 넘겨받음) | 따로 집계하며 **승률에 넣지 않음** |
| 게임 시계가 오랫동안 멈춤 | 미정 |
승패가 가려지면 러너가 결과 화면을 닫고 "다음 게임 설정"에 따라 다음 게임을 시작하며, 스킴이 이어서 넘겨받습니다 — 밤새 켜 두고 전적을 쌓을 수 있습니다. 일시정지는 종료가 아닙니다. 일시정지 중에도 Bot은 평소처럼 돌아가고, 게임 시계만 멈춥니다.
## 신뢰와 보안
**스킴은 코드이며, 실행되면 여러분 본인과 같은 권한을 가집니다**(파일을 읽고 쓰고, 네트워크에 접속할 수 있음). 그래서:
- `installed/`의 스킴은 기본적으로 **신뢰되지 않으며**, 여러분이 "신뢰"를 누를 때까지 Farsight와 러너 모두 실행을 거부합니다.
- 같은 id의 스킴을 교체 설치하면 **신뢰가 초기화됩니다**(새 버전 = 새 코드).
- 가져올 때 검사합니다: zip은 50 MB 이하, 파일 2000개 이하. 절대 경로와 `..`는 허용하지 않습니다(스킴 폴더 밖에 쓰는 것을 방지). 매니페스트가 올바르지 않거나 진입 파일이 없으면 바로 거부합니다.
> **주의**
>
> 신뢰하기 전에 "폴더 열기"로 코드를 한 번 읽어 보세요. 믿을 수 있는 사람에게서만 스킴을 받으세요.
## API(스크립트용)
| API | 설명 |
|---|---|
| `GET /api/schemes` | 스킴 목록 + 전적 + 인스턴스별로 선택된 스킴과 실행 중인 스킴 |
| `GET /api/schemes/results?ref=` | 한 스킴의 최근 30게임 |
| `POST /api/schemes/import` | zip 가져오기 |
| `GET /api/schemes/export?ref=` | zip 다운로드 |
| `POST /api/schemes/fork` | 내 스킴으로 복사 |
| `POST /api/schemes/trust` | 신뢰 |
| `DELETE /api/schemes?ref=` | 삭제(인스턴스가 사용 중이면 거부) |
| `POST /api/instances/{n}/scheme` | 인스턴스의 스킴 변경: 이 게임을 즉시 넘겨받거나, 다음 테스트 시작부터 적용 |
Python에서는 라이브러리를 바로 쓰면 됩니다: `from openwar3 import schemes`(`list_schemes`, `install_zip`, `export_zip`, `fork`, `trust`, `stats`……).
## 앞으로: 스킴 웹사이트
내보낸 zip이 곧 공유 단위이므로, 웹사이트는 그 바깥에 한 겹만 더하면 됩니다. Farsight에서 클릭 한 번으로 업로드하고, 웹사이트에서 받은 스킴은 "스킴 가져오기"와 완전히 같은 검사를 거치며 마찬가지로 신뢰를 확인해야 합니다. 전적 보고를 선택할 수 있고, 웹사이트는 버전별로 승률을 집계합니다. Farsight의 "스킴 웹사이트에 공유" 버튼은 이미 자리를 마련해 두었습니다. 진행 상황은 [로드맵](https://war3ai.com/ko/roadmap/)을 보세요.
---
# 말풍선과 로컬 모델
> 게임 속 어떤 유닛이든 원하는 신분으로 머리 위에 대화 말풍선을 띄웁니다. 로컬 LLM을 연결하면 한 마디가 들어가고 답변 한 마디가 유닛 머리 위에 나타납니다.
말풍선은 관전용 레이어입니다. 승패에 영향을 주지 않으며 방송, 해설, 디버깅에 적합합니다.
- 어떤 유닛이든, 어떤 신분으로든 말할 수 있고, 여러 유닛이 동시에 말할 수도 있습니다.
- 말풍선마다 글자 크기, 색상, 너비, 꼬리, 투명도, 타이핑 속도를 따로 설정할 수 있습니다.
- 로컬 LLM(LM Studio)에 바로 연결할 수 있고 스트리밍 출력을 지원합니다. 생성되는 대로 말풍선이 갱신됩니다.
## Bot에서 사용하기
가장 간단한 방법은 SDK에 내장된 `say`입니다.
```python
g.say(hero, "나를 따르라!", seconds=4)
```
## 시작과 화면
**가장 간단한 방법은 Farsight 첫 화면 "컨트롤 센터"입니다.** 먼저 "로컬 LLM → 시작하고 모델 로드"를 누르고(LM Studio 로컬 서버 + 설정해 둔 모델을 VRAM에 로드), 그다음 "머리 위 채팅 말풍선 → 시작"을 누르세요. 카드에서 로그 보기, 중지, 재시작도 할 수 있습니다.
화면은 Farsight 왼쪽의 "머리 위 말풍선" 페이지에 있습니다. 유닛이 말하게 하기(유닛 선택, 글 입력, 스타일 조정, 모델과 대화), 일꾼 수다 모임, 카메라 대화, 전황 트리거, 모델 설정을 제공하며, 상단 바에서 고른 인스턴스를 대상으로 동작합니다.
명령줄로도 할 수 있습니다:
```bash
python speech/speak_launch.py # 로컬 모델 서버 시작 + 모델 로드 및 워밍업 + 말풍선 API 시작
python speech/speak_launch.py --restart # 코드를 고친 뒤 API 재시작
python speech/speak_launch.py --stop # API를 멈추고 모델을 VRAM에서 내림
```
모든 단계가 "이미 있으면 건너뛰기"이므로 여러 번 실행해도 부작용이 없습니다.
## HTTP API
기본 주소는 `http://127.0.0.1:8872/`입니다(포트는 `openwar3.json`의 `ports.speech`). 어떤 프로그램이든 호출할 수 있습니다.
### 유닛이 말하게 하기 `POST /api/say`
```json
{
"inst": 16,
"bubbles": [
{ "unit": "0x14A12614", "name": "마운틴 킹", "text": "나를 따르라!" },
{ "unit": "0x14A12924", "name": "아크메이지", "text": "눈보라는 내가 맡지.",
"style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
{ "screen": [960, 110], "key": 1, "name": "내레이션", "text": "첫 번째 오크 부대가 30초 후 도착합니다.",
"style": { "tail": false, "type_ms": 0 } },
{ "world": [-4684, 2644], "key": 2, "text": "집결 지점", "style": { "font_px": 16 } }
]
}
```
| 필드 | 설명 |
|---|---|
| `unit` / `world` / `screen` | 셋 중 하나: 유닛을 따라다님(체력 바가 있으면 체력 바 바로 위에 붙음) / 맵 좌표 / 화면 픽셀(내레이션용) |
| `name` | 첫 줄에 표시되는 화자. 자유롭게 쓰면 되며 해당 유닛일 필요는 없음 |
| `text` | 본문. 자동 줄바꿈 |
| `duration_ms` | 표시 시간. 0 = 자동 3 ~ 5초 |
| `key` | 월드 / 화면 말풍선의 번호. 같은 key의 새 메시지가 이전 메시지를 대체 |
| `update` | 같은 말풍선이 이미 있으면 글자만 바꾸고 타이머는 초기화하지 않음(스트리밍 출력용) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`…… |
말풍선은 동시에 최대 32개까지 띄울 수 있으며, 프레임당 오버헤드는 평균 약 0.1 ~ 0.2 ms입니다.
### 로컬 모델과 대화하기 `POST /api/chat`
```json
{
"inst": 16, "unit": "0x14A12614", "name": "마운틴 킹",
"persona": "너는 워크래프트의 마운틴 킹 무라딘을 연기한다. 호탕하고 술을 좋아한다. 구어체로 한두 문장, 40자 이내.",
"message": "앞에 오우거 무리가 있는데, 돌격할까?",
"stream": true
}
```
`{"reply": "...", "first_token_ms": 283, "total_ms": 342}`를 반환하며, 그와 동시에 답변이 이미 해당 유닛 머리 위에 나타나 있습니다. 같은 유닛은 최근 6턴의 대화를 기억합니다.
### 기타
| API | 설명 |
|---|---|
| `GET /api/instances` | 실행 중인 게임 |
| `GET /api/units?inst=16&mine=true&heroes=true` | 유닛 목록(중국어 이름, 좌표, 체력 포함) |
| `POST /api/clear` | 말풍선 하나 또는 전부 지우기 |
| `GET /api/llm`, `POST /api/llm` | 모델 설정 보기 / 변경(`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | 일꾼 수다 모임: 본진의 일꾼들이 캐릭터 설정에 따라 돌아가며 투덜거리고, 게임 시작 때 소개 멘트를 함(전황은 모두 실제 데이터) |
| `POST /api/camtalk` | 카메라 대화: 화면에 잡힌 영웅과 수행원이 신분에 맞게 대화 |
| `POST /api/events` | 전황 트리거: 교전 시작, 교전 종료, 영웅 사망, 티어 업, 건물 파괴…… 일이 생겼을 때만 말함 |
## 로컬 모델 고르기
RTX 5090 한 장에서 실측한 결과입니다(게임 대사 5문장).
| 모델 | VRAM | 속도 | 답변 1회 | 결론 |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B**(MoE, 매번 3B만 활성화), Q4, 사고 끔 | 20.6 GB | 약 142 token/s | **약 0.3초**(첫 토큰 약 0.27초) | 추천: 빠르고, 중국어 롤플레이가 자연스러움 |
| gpt-oss-20b(MXFP4), 추론 low | 11.3 GB | 약 280 token/s | 0.3 ~ 0.8초 | VRAM이 빠듯할 때 사용. 중국어가 다소 밋밋함 |
| Qwen3.6-27B(dense), Q4 | 17.2 GB | 약 39 token/s | 5.5초가 지나도 아직 사고 중 | 실시간 대화에 부적합 |
- **속도는 총 파라미터가 아니라 "매번 활성화되는 파라미터 수"가 결정합니다**: 35B MoE는 3B만 활성화하므로 27B dense 모델보다 3 ~ 4배 빠릅니다.
- **"사고(thinking)"는 반드시 꺼야 합니다**: 끄지 않으면 토큰을 모두 사고에 써 버려 답변이 한 글자도 나오지 않습니다.
- 말풍선은 초당 약 22자씩 한 글자씩 찍히므로 생성 속도는 더 이상 병목이 아닙니다. 체감에 실제로 영향을 주는 것은 **첫 토큰 지연**입니다.
> **대사를 “진짜처럼” 만들기**
>
> 모델에 주는 전황은 모두 실제 데이터(경기 수, 승패, 병력, 보유 자원)로 채우고, "이 사실만 사용할 것"을 명시하세요. 실측해 보니 이 제한을 두지 않으면 모델이 일어나지도 않은 전투를 지어냈습니다.
---
# 게이트웨이
> WebSocket / JSON 게이트웨이: Python SDK로 호출할 수 있는 공개 API를 JS, C#, Go, Rust, 브라우저 페이지, 다른 컴퓨터의 프로그램에서도 호출할 수 있습니다. 세 가지 역할, JS 클라이언트와 브라우저 데모 페이지 포함. 지연은 고속 레인에 약 1 ms가 더해지는 정도입니다.
게이트웨이는 고속 레인과 푸시 상태를 **WebSocket / JSON**으로 감쌉니다. [API 카탈로그](https://war3ai.com/ko/api/)에서 Python SDK로 호출할 수 있는 공개 API를 JS, C#, Go, Rust, 브라우저 페이지, 다른 컴퓨터의 프로그램, LLM에서도 호출할 수 있으며, 메서드 이름과 인자도 같습니다. 지연은 고속 레인에 약 1 ms가 더해지는 정도입니다.
**가장 간단한 방법: Farsight 첫 화면 "컨트롤 센터" → 게이트웨이 → 시작**(중지, 재시작, 로그 보기, 데모 페이지 열기도 그 카드에서 합니다). 명령줄:
```bash
python gateway/server.py # ws://127.0.0.1:8870/ws(포트는 openwar3.json의 ports.gateway)
python gateway/server.py --open # 위와 같고, 포트가 열리면 데모 페이지 http://127.0.0.1:8870/demo를 엶
python gateway/server.py --host 0.0.0.0 # LAN용: 토큰을 자동으로 요구(bin/gateway/token.txt)
python gateway/server.py --allow-origin http://localhost:5173 # 직접 만든 웹 페이지에서도 연결 가능
```
## 연결과 역할
연결 주소: `ws://127.0.0.1:8870/ws?inst=9&role=dev`(`inst=` 대신 `pid=`를 써도 됩니다. 토큰이 필요하면 `&token=`을 붙입니다).
| 역할 | 호출할 수 있는 것 | 적합한 용도 |
|---|---|---|
| `dev` | 전부: 관찰, 명령, 게임 제어, 샌드박스(JASS로 월드 변경), UI 그리기 | 로컬 도구, [게임플레이 모드](https://war3ai.com/ko/docs/mods/), 동료 |
| `player`(`&player=N` 추가) | 관찰, N번 플레이어의 유닛 지휘, UI 그리기. **기본값은 공정 모드**로, N번 플레이어의 시야 안에 있는 것만 보임(`&fair=0`으로 끔) | 특정 플레이어 대신 출전하는 Bot이나 LLM |
| `observer` | 읽기 전용(내린 명령은 런타임이 바로 거부) | 관전, 해설, 데이터 수집 |
`player`가 쓸 수 없는 것: 게임 종료, 속도 변경, 일시 정지 같은 게임 제어, 다른 플레이어의 패를 볼 수 있는 `players`와 `enemy_ai_plan`, 게임 프로세스가 로컬 파일을 열게 하는 `canvas.image`, 그리고 JASS. `resources`, `tech`, `stats`처럼 플레이어 번호를 받는 조회는 자기 자신만 조회할 수 있습니다.
연결 하나가 세션 하나이며, 고속 레인 하나를 차지합니다(런타임에는 모두 16개). 게이트웨이는 동시에 최대 12개 세션까지 받아 Bot, 모드, Farsight 몫으로 몇 개를 남겨 둡니다. 연결이 끊기면 그 세션이 직접 그린 것과 단축키만 정리하고, 다른 프로그램이 그린 것은 건드리지 않습니다.
## 메시지
연결되면 먼저 `hello`를 받습니다: 프로토콜 버전, 역할, 게임 프로세스 ID, 이 역할이 호출할 수 있는 메서드 목록. 이후 요청마다 `id`를 붙이며, 응답에는 같은 `id`가 붙습니다.
```json
→ {"id": 1, "op": "call", "method": "units", "args": ["me"]}
← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]}
→ {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]}
→ {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "물약 사기"], "kwargs": {"screen": [40, 300]}}
→ {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true}
← {"type": "state", ...} {"type": "events", ...} 이후 계속 푸시
→ {"id": 5, "op": "overview"} 게임 상황 한 페이지: 자원, 유닛 종류별 수, 영웅, 보이는 적, 생산
→ {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"} dev 전용
→ {"id": 7, "op": "api"} 메서드 카탈로그(그 밖에 ping / unsubscribe)
```
- **유닛 인자**는 `{"unit": 주소}`로 씁니다. 주소는 유닛 JSON의 `addr`입니다. `"handle": [lo, hi]`를 함께 넘기면 이 주소가 다른 유닛에게 재사용되지 않았는지 확인합니다.
- **메서드 이름**은 Game의 공개 메서드이며, 여기에 `ui.*`(button / choice / toast / hotkey / mouse / cursor…), `canvas.*`(text / panel / bar / image / circle / path / remove…), `jass.<함수 이름>`(dev 전용)이 더해집니다.
- 원격에서는 콜백 함수를 넘길 수 없습니다. 클릭과 단축키는 이벤트 푸시로 받으며, `ui.click` 이벤트에 `key`가 들어 있습니다. [UI와 입력](https://war3ai.com/ko/docs/ui-input/)을 참고하세요.
- 호출 하나에서 오류가 나면 그 호출에만 오류를 돌려주며(`ok: false`와 `error`), 연결은 끊기지 않습니다. 보낸 내용이 JSON이 아닐 때도 마찬가지입니다.
- 이벤트 JSON의 필드는 [W3P 프로토콜](https://war3ai.com/ko/docs/protocol/)과 같으며, 편의 필드(`spell`, `key`, `text`, `chat`, `button`, `player`, `mods`)가 추가됩니다.
HTTP도 쓸 수 있으며, 일회성 호출이나 curl에 적합합니다. `GET /api?role=player`는 메서드 카탈로그를 나열하고, `POST /call`에 `inst`, `role`, `method`, `args`, `kwargs`를 담아 한 번 호출합니다. `/call`은 세션을 재사용합니다. 게임을 다시 시작해 프로세스가 바뀌면 자동으로 새 세션으로 바꾸고, 10분 동안 쓰지 않은 세션은 정리합니다.
## 클라이언트
**JS**(브라우저 또는 Node 22+, 의존성 없음): `gateway/clients/js/openwar3.mjs`
```js
import { OpenWar3, unit } from "./openwar3.mjs";
const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" });
await ow.connect();
const mine = await ow.api.units("me");
await ow.api.move(mine.slice(0, 3).map(unit), 100, 200);
await ow.api.ui.button("hi", "눌러 보세요", { screen: [40, 300] }); // 마지막 일반 객체 = 키워드 인자
ow.on("event:ui.click", (e) => console.log("클릭됨", e.key));
await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true });
```
Node 20 / 21에서는 `--experimental-websocket`을 붙여야 합니다. 전체 예제는 `gateway/clients/js/example.mjs`에 있습니다.
**브라우저 데모 페이지** `http://127.0.0.1:8870/demo`: 게임 상황, 아군 유닛 표, 게임에 버튼 하나 놓기, 이벤트 스트림을 한 페이지에서 모두 볼 수 있습니다.
**다른 언어**: WebSocket 라이브러리 하나 + 위의 JSON이면 충분합니다. 공유 메모리를 건드릴 필요가 없습니다.
**LLM**: [MCP 서버](https://war3ai.com/ko/docs/mcp/)를 바로 쓰세요. 자주 하는 일을 미리 도구로 만들어 두었습니다.
## 실측
2026-09-25, 실제 게임에 연결해 항목별로 확인 16/16(게이트웨이 9개 + MCP 7개): 핸드셰이크(dev 역할 메서드 121개), `units('me')`, 게임 상황 한 페이지, 화면 알림, 버튼 놓기. 구독한 뒤 게임에서 그 버튼을 클릭 → `ui.click`이 클라이언트로 푸시됨. JASS. 잘못된 유닛을 넘기면 그 호출에만 오류를 보고. HTTP `/call`(observer 역할).
JS 클라이언트(Node)와 브라우저 데모 페이지도 실행해 봤습니다. 웹 페이지에서 놓은 버튼을 게임에서 클릭하자, 웹 페이지의 이벤트 로그가 `ui.click`을 받았습니다.
## 보안
- 기본적으로 로컬 `127.0.0.1`에서만 수신 대기하며, 토큰이 필요 없습니다(Farsight와 같음). `--host`가 로컬 주소가 아니면 자동으로 토큰을 요구하고, `--auth`를 쓰면 로컬에서도 요구합니다.
- **브라우저 안의 다른 웹사이트는 연결할 수 없습니다**: 브라우저가 여는 연결에는 모두 출처(`Origin`)가 붙으며, 게이트웨이는 자체 데모 페이지와 `--allow-origin`으로 지정한 주소만 받아들입니다. Python, Node, curl 같은 프로그램은 출처를 붙이지 않으므로 평소처럼 연결됩니다. 로컬에서만 수신 대기할 때는 `Host`도 확인해, 외부 도메인을 로컬로 해석시키는 공격을 막습니다.
- 역할은 연결할 때 클라이언트가 스스로 선언합니다. 로컬 모드에서는 약속일 뿐 보안 경계가 아닙니다. 아레나에서는 누가 어떤 역할을 받을지 심판 프로세스가 정합니다. [아레나](https://war3ai.com/ko/arena/)를 참고하세요.
---
# W3P 프로토콜
> 런타임과 외부 프로그램 사이의 모든 계약입니다. 공유 메모리 여덟 블록, 월드 상태 읽기, 이벤트 읽기, 명령 내리기, 회신, 레인 역할, 캔버스, UI와 입력. Python 이외의 언어로 연동하려면 이 페이지를 보세요.
런타임과 외부 프로그램은 **공유 메모리로만** 데이터를 주고받으며, 아래 내용이 그 전부입니다.
- 참조 구현은 Python의 `sdk/python/w3world.py`(읽기)와 `sdk/python/w3fast.py`(쓰기)입니다. 모든 구조체의 크기와 오프셋이 여기에 정의되어 있으며, 테스트로 고정되어 있습니다.
- **프로토콜은 의미만 기술하며, 게임 버전과 무관합니다.** 게임 버전이 바뀌면 런타임이 알아서 맞추고 프로토콜은 그대로입니다. 새 필드는 블록 끝에만 추가하므로 기존 클라이언트도 그대로 동작합니다.
> **참고**
>
> 대부분은 이 페이지를 읽을 필요가 없습니다 — Python SDK를 쓰면 됩니다. C++ / C# / Rust / Go 같은 언어로 직접 연동하고 싶거나, SDK 아래에서 무슨 일이 일어나는지 알고 싶을 때만 필요합니다.
## 1. 공유 메모리 여덟 블록
``는 게임 프로세스 ID입니다.
| 이름 | 방향 | 내용 | 동기화 방식 |
|---|---|---|---|
| `Local\War3World_` | 런타임 → 클라이언트 | 월드 상태: 헤더 + 플레이어 16명 + 최대 1024개 유닛 + 256개 유닛 상세 정보 + 바닥 아이템 256개 + 확장 영역 + 생산 테이블 | seqlock |
| `Local\War3Trees_` | 런타임 → 클라이언트 | 최대 4096개 파괴 가능 오브젝트(나무 등), 2초마다 갱신 | seqlock |
| `Local\War3Events_` | 런타임 → 클라이언트 | 이벤트 링, 8192개 | 항목마다 시퀀스 번호 포함 |
| `Local\War3Map_` | 런타임 → 클라이언트 | 맵: 지형 격자(한 칸 128, 최대 256×256) + 플레이 가능 영역 경계 + 시작 지점. 게임 시작 후 몇 초에 걸쳐 나눠서 계산 | seqlock(계산이 끝나면 더 이상 바뀌지 않음) |
| `Local\War3Fast_` | 양방향 | 명령 레인: 16개 × 16슬롯. 슬롯마다 명령 하나 + 회신. 레인마다 역할이 있음 | 슬롯마다 단일 작성자·단일 독자 |
| `Local\War3Canvas_` | 클라이언트 → 런타임 | [캔버스](https://war3ai.com/ko/docs/canvas/): 헤더 64바이트 + 요소 256개 × 112바이트 + 64 KB 텍스트 / 점 풀. `canvas_enable`을 한 번 보낸 뒤에야 생성됨 | seqlock(클라이언트가 쓰고, 런타임이 매 프레임 읽음) |
| `Local\War3Msgs_` | 런타임 → 클라이언트 | 화면 메시지 링: 게임 안내, 채팅, 시스템 메시지의 전체 텍스트, 128개 × 256바이트 | 항목마다 시퀀스 번호 포함 |
| `Local\War3Input_` | 양방향 | [UI와 입력](https://war3ai.com/ko/docs/ui-input/): 런타임이 마우스 위치, 가리키는 지면 지점, 호버 중인 항목을 되쓰고, 클라이언트는 단축키 표와 마우스 스위치를 씀. `input_enable`을 한 번 보낸 뒤에야 런타임이 입력을 넘겨받기 시작함 | 단축키 표는 seqlock |
**여러 클라이언트가 캔버스와 입력을 동시에 쓸 때**: 이 두 블록은 하나씩밖에 없어서, 각자 따로 쓰면 서로 덮어씁니다. 약속은 다음과 같으며, 클라이언트를 직접 작성할 때도 이대로 따라야 합니다.
- **캔버스**: 이름 있는 뮤텍스 `Local\War3CanvasMutex_`를 잡은 채 읽기 - 수정 - 쓰기를 하며, 자기 요소만 바꾸고 다른 클라이언트의 요소는 그대로 둡니다(풀 오프셋은 다시 배치). 주인 프로세스가 이미 종료된 요소와 주인이 없는 요소는 지웁니다. 요소의 `reserved[1]` = 주인 프로세스 ID, `reserved[2]` = 프로세스 내 일련번호이며, 요소 번호는 블록 헤더 오프셋 60의 카운터에서 할당합니다(`0x10000`부터).
- **입력**: 각 클라이언트는 자기 단축키와 마우스 스위치를 `Local\War3InputClients_`(헤더 16바이트 + 클라이언트 16개 × 528바이트)에 등록하고, `Local\War3InputMutex_`를 잡은 채 자기 항목을 고친 뒤, 살아 있는 클라이언트들의 것을 합쳐 입력 블록에 씁니다. 단축키는 "키 코드 + 보조 키" 기준으로 중복을 없애고, 마우스 스위치는 합집합을 취합니다. 이벤트는 모든 클라이언트에 보내며, 각자 "키 코드 + 보조 키"로 자기 단축키를 알아봅니다. 등록표에 살아 있는 다른 클라이언트가 남아 있으면 `input_enable 0`을 보내지 마세요.
- **런타임**: 주인 프로세스가 종료된 클릭 가능 요소는 더 이상 클릭을 가로채지 않습니다. 2초마다 등록표를 확인해, 등록했던 클라이언트가 모두 종료되었으면 입력 블록의 단축키 표와 마우스 스위치를 0으로 지웁니다.
## 2. 월드 상태 읽기(seqlock)
```text
loop:
s1 = block.seq (오프셋 8, int32)
if s1 이 홀수: 재시도 (런타임이 쓰는 중)
헤더 + players + units[unitCount] + details[detailCount] + items[itemCount] 복사
if block.seq != s1: 재시도
```
- **헤더**: 발행 카운터(늘지 않으면 발행이 끊긴 것), 엔진 게임 시계, 게임마다 +1 되는 epoch, 자기 플레이어 번호, 게임 중인지 여부, 배속, 발행 주기, 이번 수집에 게임 스레드에서 걸린 마이크로초, 이벤트 시퀀스 번호, 구간별 소요 시간. 클라이언트는 `requestedPeriodMs`를 써서 발행 주기(16 ~ 1000 ms)를 요청할 수 있습니다.
- **유닛**(112바이트): 핸들 쌍(**유닛은 핸들 쌍으로 식별**하세요. 주소는 재사용됩니다), 종류 4자 코드, 소유자, 플래그, 좌표, 체력 / 마나(최대치 포함), 현재 오더 + 오더 대상, 작업 대상(실제로 공격 중인 대상), 영웅 레벨 / 경험치 / 스킬 포인트, 상세 정보 인덱스, `visibleTo`(비트 p = 플레이어 p가 지금 이 유닛을 볼 수 있음).
- **상세 정보**(288바이트, 영웅 > 플레이어 유닛 > 크립, 최대 256개): 스킬 12개(코드 / 레벨 / 플래그 / 남은 쿨다운 초), 버프 코드 8개, 인벤토리 6칸.
- **블록 끝 확장**(추가만 하고 앞쪽 오프셋은 옮기지 않으므로 기존 클라이언트도 그대로 동작): 확장 영역 `EXT1`(게임 내 시각, 낮밤 진행 속도, 생산 테이블 항목 수)과 생산 테이블 `prods[128]`(훈련 / 연구 / 건설 / 업그레이드 중인 건물, 대기열, 총 소요 시간, 경과 시간, 막힘 여부). **magic이 일치할 때만 사용하세요.**
## 3. 이벤트 읽기
```text
head = ring.writeSeq (오프셋 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: 하나를 놓침(너무 늦게 읽어서 덮어써짐)
elif e.seq != seq: 아직 쓰는 중, 다음에 다시 읽기
else: e 처리
```
이벤트 구조체는 64바이트입니다: `seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`.
- 인접한 두 발행을 비교해 얻는 이벤트(정밀도 = 발행 주기): `unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`
- 엔진 수준(런타임이 게임 스레드에서 발생 즉시 기록, **타격마다** 발생): `damage`(공격자, 피해 타입, 공격 타입, 위치, 실제로 깎인 체력, 방어력 적용 전 피해), `killed`(처치한 유닛)
- 생산 테이블을 추적해 얻는 이벤트: `production.done`(완료된 4자 코드, 종류, 걸린 게임 초. 상대의 것도 발생)
- 런타임이 발행할 때마다 함께 확인하는 이벤트: `spell.cast`(스킬 쿨다운 시작: `a` 스킬 4자 코드, `b` 레벨, `value` 쿨다운 초, `x/y` 시전 지점), `player.left`(`a` 플레이어 번호, `b` 새 슬롯 상태), `selection.changed`(로컬 플레이어의 선택. 전체 목록은 월드 블록 확장 영역에 있음), `game.ended`(게임에서 나감)
- 화면 메시지: `message`(`a` = 메시지 시퀀스 번호. 전체 텍스트는 공유 메모리 `Local\War3Msgs_`에서 조회: 128개 × 256바이트, 게임 안내, 채팅, 시스템 메시지가 모두 들어 있음. `b` = 메시지 영역 번호)
- UI와 입력(`input_enable`을 켠 뒤): `ui.click`(`a` 캔버스 항목 id, `b` 1 왼쪽 버튼 / 2 오른쪽 버튼), `ui.hover`, `hotkey`(`a` 단축키 id, `b` 가상 키 코드), `mouse.world`(`x/y` 지면 좌표, `value` = 1이면 가로챔). 보조 키는 모두 `extra`에 있음
## 4. 명령 내리기
1. **클라이언트 객체 하나가 레인 하나를 차지합니다**: `Local\War3FastMutex_`를 잡고 비어 있는(또는 주인 프로세스가 죽은) 레인을 찾아, 역할, 플레이어 번호, 자신의 pid를 씁니다. 한 프로세스에서 두 역할이 필요하면 레인을 두 개 엽니다.
2. 슬롯 채우기: 시맨틱 명령 플래그, 연산 코드, `args[11]`, 마감 시각 `deadlineMs`.
3. 모든 슬롯을 다 쓴 뒤 제출 표시를 하고, 레인의 `submitSeq`를 1 늘립니다.
4. `Local\War3FastDone__` 이벤트를 기다리고(또는 폴링), 회신을 읽은 뒤 슬롯을 반납합니다.
런타임은 게임 스레드의 이벤트 디스패치 안에서 명령을 모아 실행합니다. 한 번 비우는 데 쓰는 시간 예산은 **4 ms**(실제 고정밀 타이머)이며, 넘으면 나머지는 다음 디스패치로 넘깁니다. **마감 시각이 지난 슬롯은 더 이상 실행되지 않습니다** — "일시정지를 풀었더니 예전 명령이 다시 실행되는" 일은 생기지 않습니다.
`args` 인덱스: `0..2` 유닛(주소, 핸들 lo, 핸들 hi), `3` 오더 ID 또는 4자 코드, `4..6` 대상, `7/8` x / y(float 비트), `9` extra(플레이어 번호 / 칸 번호 / 스위치 / 대기열 비트), `10` mode(0 대상 없음 / 1 지점 대상 / 2 유닛 대상).
### 연산 코드
| 연산 코드 | 이름 | 설명 |
|---|---|---|
| 1 | `point` | 유닛에게 지점 대상 명령(이동 / 공격 이동 / 순찰 / 지면 공격 / 지점 시전). extra bit0 = 대기열(현재 오더 뒤에 삽입) |
| 2 | `target` | 유닛에게 대상 지정 명령(우클릭 공격 / 채집 / 수리 / 대상 시전 / 아이템 줍기). 대상은 반드시 보여야 함 |
| 3 | `immediate` | 대상 없는 명령(정지 / 위치 사수 / 훈련 / 연구 / 업그레이드 / 대상 없는 시전) |
| 4 | `build` | 일꾼의 건물 건설(좌표는 32 단위로 정렬) |
| 5 | `learn` | 영웅의 스킬 습득 |
| 6 | `use_item` | 인벤토리 extra번째 칸 사용 |
| 7 | `revive` | 제단에서 영웅 부활 |
| 8 | `rally` | 집결 지점(지점 대상 / 유닛 대상) |
| 9 | `buy` | 상점이 옆에 있는 영웅에게 아이템 판매 |
| 10 | `item_drop` | 아이템을 손에서 놓기: 아군에게 주기, 상점에 팔기(`code` = 받는 유닛), 또는 바닥에 버리기 |
| 20 ~ 25 | 쿼리 | `q_tech` 기술 카운트, `q_feasible` 실행 가능성, `q_visible` 가시성, `q_mine_gold` 금광 잔량, `q_captain` 컴퓨터 대장, `q_dead_heroes` 사망 영웅 목록 |
| 30 | `pause` | 일시정지 / 재개 |
| 40 ~ 50 | 카메라 | 카메라 상태 읽기, 필드 설정, 한 지점 바라보기, 따라가기, 초기화, 회전, 경계, 부드러운 이동, UI 표시/숨김, 깨끗한 화면, 안개 |
| 60 ~ 63 | HUD | 퀘스트 버튼 텍스트, 퀘스트 패널 제목과 설명, 새로 고침, 패널이 열렸는지 읽기 |
| 70 | `jass` | JASS native를 이름으로 호출(1291개). 이름과 문자열 인수는 슬롯의 부가 영역에, 나머지 인수는 시그니처에 따라 `args`에 넣음. 반환값은 `value[0]`. 로컬 도구 레인 전용이며, 함수 인수를 받거나 스크립트 스레드를 일시 중단시킬 수 있는 native는 모두 거부. [JASS 채널](https://war3ai.com/ko/docs/jass/) 참고 |
| 71 / 72 | `jass_handle_of` / `jass_unit_of` | 스냅샷 속 유닛 ↔ JASS 핸들 상호 변환(스냅샷의 핸들 쌍은 JASS 핸들이 아님) |
| 73 | `canvas_enable` | 캔버스 공유 메모리를 만들고 그리기 후크를 설치. 어떤 레인에서든 보낼 수 있음(캔버스는 로컬 화면에만 그림). 처음에는 후크를 설치해야 하므로 타임아웃을 2초 이상으로 |
| 74 | `input_enable` | `extra` = 1이면 게임 창의 입력(캔버스 항목 클릭 / 호버, 단축키, 지면 클릭)을 넘겨받고, 0이면 돌려줌. 입력 블록 `Local\War3Input_`: 헤더 128바이트 + 단축키 32개 × 16바이트. 클라이언트는 단축키 표와 마우스 스위치를 쓰고, 런타임은 마우스 위치, 가리키는 지면 지점, 호버 중인 항목을 되씀. 어떤 레인에서든 보낼 수 있음(로컬 입력에만 영향). [UI와 입력](https://war3ai.com/ko/docs/ui-input/) 참고 |
## 5. 회신
회신은 52바이트(+ 소요 시간 8바이트)입니다: `status`, `engineReturn`, `verdict`(거부 원인 코드), `orderBefore / orderAfter`(같은 프레임에 다시 읽은 유닛 오더), `value[8]`(쿼리 결과), `execUs`(이 명령이 게임 스레드에서 실행된 마이크로초), `engineUs`(그중 엔진의 명령 함수 자체가 걸린 시간).
모든 상태 코드와 원인 코드는 [회신과 원인 코드](https://war3ai.com/ko/docs/reason-codes/)를 참고하세요.
## 6. 레인 역할
| 역할 | 할 수 있는 것 |
|---|---|
| `dev` | 로컬 도구: 시맨틱 명령(로컬 플레이어의 유닛 지휘) + JASS 채널 |
| `player` | 시맨틱 명령만 가능하며, 레인이 속한 플레이어의 유닛만 지휘 가능(다른 플레이어의 유닛 = `not_owner`) |
| `observer` | 쿼리, 카메라, HUD 패널 상태 읽기, 캔버스와 로컬 입력 켜기만 가능. 나머지는 모두 `forbidden` |
AI 두 개의 대결 = 한 게임에서 `player` 레인 두 개(player 0 / player 1)를 엽니다.
> **주의**
>
> 로컬 모드에서는 클라이언트가 역할을 스스로 선언합니다(약속일 뿐 보안 경계가 아닙니다). [아레나](https://war3ai.com/ko/arena/)에서는 심판 프로세스가 레인을 만들고, 선수에게는 `player` 레인만 넘깁니다.
## 7. 실측으로 확인한 동작
- 적에게 우클릭(smart) = **그 대상 하나**를 공격합니다(오더 대상, 작업 대상 모두 그 대상). 원시 공격 오더는 대상 명령을 거쳐도 공격 오더만 바뀌고 대상을 기억하지 않아, 근처의 다른 적을 공격하러 갑니다.
- 엔진은 보이지 않는 유닛에게 대상 명령을 허용하지 않습니다. 해가 진 뒤 먼 캠프가 안개에 들어가면 우클릭은 모두 거부됩니다(1001).
- 건설 "수락"은 일꾼이 명령을 받았다는 뜻일 뿐입니다. 숲 속 지점도 즉시 수락되고, 일꾼이 도착해서야 실패합니다. 명백히 점유된 지점은 즉시 거부됩니다.
- 영웅은 사망 후 약 3 게임 초가 지나야 부활할 수 있습니다. 인구가 부족해도 거부됩니다(영웅도 인구를 차지함).
- 인벤토리의 아이템은 바닥 아이템에 포함되지 않습니다. 주우면 `item.removed`가 발생합니다.
- 일시정지 중에는 엔진 시계가 멈추지만, 명령은 평소처럼 내릴 수 있습니다.
- 최소화 상태로 띄운 게임은 시뮬레이션이 멈춰 있습니다(시계가 흐르지 않음).
---
# 회신과 사유 코드
> 모든 명령의 회신에는 상태 코드와 사유 코드가 담깁니다. Bot과 Agent가 스스로 오류를 고치는 근거로, "왜 안 됐는가"를 기계가 읽을 수 있는 숫자로 바꿔 줍니다.
```python
r = g.train(barracks, "hfoo")
bool(r) # False
r.status # 1 -> rejected
r.verdict # 3 -> 식량 부족
r.reason # 'rejected(人口不够)' = 식량 부족
r.exec_us # 이 명령이 게임 스레드에서 실행된 시간(마이크로초)
```
`if r:`은 `r.status == 0`(엔진이 수락함)과 같습니다.
## 상태 코드 `status`
| 코드 | 이름 | 의미 | 흔한 원인 |
|---|---|---|---|
| 0 | `accepted` | 엔진이 수락함 | —(단, 수락 ≠ 완료, 아래 참고) |
| 1 | `rejected` | 엔진이 거부함 | `verdict` 확인 |
| 2 | `bad_unit` | 유닛이 없거나 핸들이 맞지 않음 | 유닛이 이미 죽음, 만료된 유닛 객체를 사용함 |
| 3 | `not_owner` | 내 유닛이 아님 | `player` 역할로 다른 플레이어의 유닛을 지휘함 |
| 4 | `fault` | 실행 중 예외(런타임이 이미 막아 두어 게임을 무너뜨리지 않음) | 재현 단계와 함께 제보해 주세요 |
| 5 | `bad_args` | 인수 오류 | 좌표, 칸 번호, 4자리 코드를 잘못 씀 |
| 6 | `unsupported` | 지원하지 않음 | 이 버전의 런타임에 해당 기능이 없음 |
| 7 | `bad_target` | 잘못된 대상 | 대상이 이미 사라짐, 대상 유형이 맞지 않음 |
| 8 | `forbidden` | 레인 역할상 허용되지 않음 | `observer` 역할로 명령함 |
| 97 | `cancelled` | 배치 블록에서 예외가 발생해 배치 전체가 전송되지 않음 | `with g.batch():` 블록 안의 코드에서 오류 발생 |
| 98 | `held` | 더 높은 우선순위의 레이어가 유닛을 점유하고 있어 전송되지 않음 | 레퍼런스 브레인의 반사 레이어나 콘솔의 수동 명령이 이 유닛을 점유 중 |
| 99 | `timeout` | 시간 초과 | 게임 일시 정지 / 랙으로 마감 시간을 넘김(만료된 명령은 다시 실행되지 않음) |
## 사유 코드 `verdict`
거부되면 런타임이 엔진 자체의 실행 가능성 검사로 이유를 알려 줍니다. 명령을 내리지 않고 먼저 물어볼 수도 있습니다. `g.can_do(유닛, 4자리코드)`는 같은 코드를 반환합니다.
| 코드 | 의미 | 대처 |
|---|---|---|
| 0 / 220 | 가능 | — |
| 3 | 식량 부족 | 식량 건물 건설. `g.production(b).blocked`로 미리 감지 |
| 8 | 금 부족 | 돈이 모일 때까지 대기. 명령 전에 `g.can_afford(code)` 사용 |
| 9 | 목재 부족 | 벌목 인원 추가 |
| 32 | 훈련 대기열 가득 참(7칸) | 대기열에는 1개만: `g.queue(b)`가 비면 다음 것을 넣기 |
| 183 | 선행 기술 / 건물 부족 | 선행 건물을 먼저 짓거나 티어 업 |
| 185 | 건물이 사용 중 | 제단에서 영웅 부활 중. 본진 건물 대기열이 비어 있지 않으면 업그레이드 불가 |
| 221 | 해당 항목 없음 / 건설 중 / 업그레이드 중 / 이미 존재 | 영웅이 이미 있음(죽었으면 `revive`). 이 상점은 해당 품목을 팔지 않음 |
| 89 | 상점에 아직 입고되지 않음 | 게임 시작 후 아이템 테이블의 입고 시간이 지나야 재고가 생김. 새로 지은 상점은 완공 시점부터 계산 |
| 1001 | 대상이 보이지 않음 | 대상이 전장의 안개나 검은 마스크 안에 있음. 그 위치로 `attack_move` |
## 수락 ≠ 완료
회신은 "엔진이 이 명령을 받아들였다"는 것만 알려 주며, 같은 프레임 안에서 읽어 온 결과입니다. 그 뒤에 일어날 수 있는 일까지는 알 수 없습니다.
| 명령 | 회신이 수락된 뒤에도 실패할 수 있는 경우 | 확인 방법 |
|---|---|---|
| 건설 | 숲속 지점도 그 자리에서는 수락되고, 일꾼이 도착해서야 실패 | `build_near` 사용(건설 부지가 나타나는지 추적), 또는 `production.done` 대기 |
| 시전 | 끊김, 마나 부족 | 다음 틱에 `g.cooldown(u, 스킬)`이 쿨다운에 들어갔는지 확인 |
| 훈련 | 대기열에 들어갔지만 식량이 부족해 계속 시작되지 않음 | `g.production(b).blocked` |
| 이동 / 공격 | 다른 로직(또는 더 높은 우선순위의 레이어)이 덮어씀 | `g.current_target(u)`, `g.order_of(u)` |
## 조회 API
아래 API는 명령을 내리지 않고 엔진에 묻기만 합니다. 결과는 회신의 `value`에도 담깁니다(SDK는 값을 바로 반환합니다).
| API | 반환 |
|---|---|
| `g.can_do(u, code)` / `g.can_do_many([(u, code), ...])` | 위 표의 사유 코드 |
| `g.tech(code, player=None)` / `g.tech_many([...])` | 연구 레벨 / 완성된 건물 수(업그레이드 체인 포함) |
| `g.visible(x, y)` | 이 지점이 아군에게 보이는지 |
| `g.gold_left(mine)` | 금광에 남은 금 |
| `g.enemy_ai_plan(적 유닛)` | 컴퓨터 상대의 부대장이 병력을 이끌고 어디로 가려는지(컴퓨터 AI에만 유효) |
---
# 데이터 출처
> 데이터 종류별 출처와 정밀도입니다. "뭔가 이상해 보이는" 일이 생기면 이 페이지부터 확인하세요.
| 데이터 | 출처 | 정밀도 |
|---|---|---|
| 유닛, 자원, 오더, 스킬, 버프, 인벤토리 | 런타임이 50 ms마다 푸시하는 월드 블록 | 발행 주기(16 ms까지 조정 가능) |
| 피해, 처치 이벤트 | 런타임이 게임 스레드에서 즉시 기록, 타격마다 | 즉시 |
| 기타 이벤트(등장, 사망, 오더 변경, 레벨 업……) | 인접한 두 발행을 비교 | 발행 주기 |
| 생산 테이블(훈련 / 연구 / 건설 / 업그레이드) | 엔진 생산 스킬의 타이머 필드 + 런타임이 누적한 경과 시간 | 약 ±0.2게임초 |
| 전투 속성, 상성표 | 게임 자체 데이터 테이블(여러분의 게임에서 추출) | 아이템, 오라, 버프 보정은 미포함 |
| 경로 탐색 | 엔진의 지형 이동 가능성(128 단위 한 칸) + 나무 + 건물 점유 영역, SDK 측 A* | 한 칸. 한 칸보다 좁은 틈은 막힌 것으로 판정 |
| 게임 내 시각 | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | 발행 주기 |
| 가시성 | 런타임이 유닛마다 플레이어별로 계산한 가시성 마스크 | 발행 주기 |
| 기술 카운트, 실행 가능성, 금광 잔량 | 고속 레인 조회로 엔진에 직접 질의 | 즉시 |
## 게임 데이터는 코드와 함께 배포하지 않습니다
유닛 테이블, 스킬, 아이템, 영웅, 버프, 피해 상성표 등은 블리자드의 게임 파일에서 나오며 **저장소에 포함되지 않습니다**. Farsight "컨트롤 센터"에서 게임 디렉터리를 설정하면 여러분의 게임에서 자동으로 추출하며, 직접 실행할 수도 있습니다:
```bash
python data/tools/extract_game_data.py
```
추출 결과는 `data/game/`(git에 포함되지 않음)에 저장됩니다. 원본 `.slk` / `.txt`, 그리고 정리된 `units.json`, `names.json`, `skills.json`, `items.json`, `heroes.json`, `buffs.json`입니다.
## 몇 가지 구체적인 수치
| 항목 | 값 |
|---|---|
| 하루 | 480게임초(낮, 밤 각 240초), 1시간 = 20게임초. 게임은 오전 8시에 시작 |
| 낮 | 6:00 ~ 18:00 |
| 방어력 계수 | 0.06(게임 데이터 테이블 기준) |
| 월드 블록 용량 | 플레이어 16명, 유닛 1024개, 유닛 세부 정보 256개, 바닥 아이템 256개, 생산 128건 |
| 나무 | 파괴 가능한 오브젝트 최대 4096개, 2초마다 갱신 |
| 이벤트 링 | 8192건. 너무 느리게 읽으면 유실(SDK가 감지 가능) |
| 맵 그리드 | 한 칸 = 게임 단위 128, 최대 256 × 256 |
## 실측으로 보정한 예
- 생산 시간: 일꾼 14.9, 농장 34.9, Iron Forged Swords 59.9게임초로, 런타임이 푸시한 값과 일치합니다(오차 0.2초 미만).
- 전투 속성을 게임 패널과 대조: 팔라딘 체력 650, 마나 255, 방어력 3.9, 공격력 24 ~ 34. 공격 업그레이드 1단계 보병 13 ~ 15.
- 엔진 피해 이벤트의 방어력 적용 전 피해(14 / 15 / 15)가 `stats()`로 계산한 범위 안에 들어갑니다.
- 경로 탐색: Echo Isles 116 × 88칸, 상대 본진 건물까지 지상 거리 10642(직선 9856), 그리드 생성 18 ms, A* 1회 약 1 ms.
---
# 자주 묻는 질문
> 핵인가요? 어떤 버전을 지원하나요? AI는 무엇을 보고 무엇을 할 수 있나요? 프로그래밍을 몰라도 쓸 수 있나요?……
## 핵인가요?
아닙니다. AI 연구와 오락을 위한 개발 인터페이스로, **여러분이 합법적으로 소유한 클라이언트**에서만 사용하며 로컬, LAN, 직접 만든 게임에서 컴퓨터나 다른 AI와 대전하는 용도입니다. **Battle.net이나 안티치트가 있는 어떤 서버에서도 사용할 수 없으며**, 사람 간 대전을 겨냥한 기능도 전혀 제공하지 않습니다. 자세한 내용은 [이용 범위](https://war3ai.com/ko/docs/legal/)를 참고하세요.
## 어떤 게임 버전을 지원하나요?
현재는 **워크래프트 III 1.27**(프로즌 쓰론)만 지원합니다. 1.24 ~ 1.28은 같은 엔진 구조이며, 다중 버전 지원(버전별 심볼 테이블 선택, 시그니처 스캔 폴백, 시작 시 자가 점검으로 기능 목록 생성)은 [로드맵](https://war3ai.com/ko/roadmap/)의 P4 단계에 있습니다. 1.29 이후 버전과 리포지드는 다른 엔진이라 별도 지원이 필요하며, 현재는 약속하지 않습니다.
## 게임 파일을 수정하나요?
아닙니다. 런타임은 게임 실행 중에 주입되며, **디스크의 Game.dll**이나 어떤 게임 파일도 **수정하지 않습니다**. 여러 개를 띄울 때도 원본 `War3.exe` 런처를 그대로 복사해 이름만 바꿉니다. 게임 데이터(유닛 테이블 등)는 여러분의 게임에서 추출하며, 코드와 함께 배포하지 않습니다.
## AI는 무엇을 볼 수 있나요?
기본적으로 프로 선수가 알고 싶어 하는 모든 것이며, 50 ms마다 갱신됩니다.
- 모든 플레이어의 금, 목재, 식량. 모든 유닛의 위치, 체력/마나, 현재 오더, **현재 공격 대상**, 레벨과 경험치
- 영웅과 유닛의 스킬 레벨과 남은 쿨다운, 걸려 있는 버프, 인벤토리
- 건물마다 무엇을 훈련 / 연구 / 건설 / 업그레이드하고 있는지, 진행률, 식량 때문에 막혔는지
- 바닥의 아이템, 나무, 맵의 이동 가능 / 건설 가능 그리드, 시작 위치, 게임 내 시각(낮과 밤)
- 이벤트 스트림: 유닛 등장과 사망, **피해 한 번 한 번**(누가 공격했는지, 공격 유형, 방어력 적용 전 피해), 처치, 생산 완료, 영웅 레벨 업……
- 엔진에 직접 물어볼 수도 있습니다. 어떤 일을 지금 할 수 있는지, 왜 안 되는지, 어떤 기술이 몇 레벨인지, 어떤 지점이 보이는지, 금광에 금이 얼마나 남았는지, 컴퓨터 상대가 병력을 이끌고 어디를 치려는지.
그 위에 SDK가 전투 속성(상성, 방어력, 공격/방어 업그레이드), "처치까지 몇 초 걸리는지", 지상 경로 탐색까지 계산해 둡니다. 전체 API는 [API 카탈로그](https://war3ai.com/ko/api/)에서 볼 수 있습니다.
## AI는 무엇을 할 수 있나요?
플레이어가 할 수 있는 조작은 거의 모두 가능합니다. 이동, 공격 이동, 지정 대상 공격, 정지, 위치 사수, 순찰, 지면 공격, 채집, 수리, 건설(위치 자동 탐색 가능), 훈련 / 연구 / 업그레이드, 취소, 스킬 습득, 시전(유닛 대상 / 지점 대상 / 대상 없음), 집결 지점, 영웅 부활, 아이템 줍기 / 사용 / 버리기 / 건네기 / 팔기, 구매, Call to Arms. Shift 대기열, 경유점 행군, 일꾼 한 명이 여러 채를 연속으로 짓기도 됩니다. 게임 속도, 일시 정지, 머리 위 말풍선도 있습니다. 모든 명령에는 회신이 있습니다.
플레이어의 조작 외에도, 게임 화면 위에 나만의 패널과 표시를 그리거나([캔버스](https://war3ai.com/ko/docs/canvas/)), 싱글플레이 게임에서 맵 제작자가 쓸 수 있는 JASS 함수 1291개를 호출할 수 있습니다([JASS 채널](https://war3ai.com/ko/docs/jass/)).
## RPG / 커스텀 맵에서도 쓸 수 있나요?
네. RPG 맵을 고르고 인스턴스에 "동료 예제(buddy)" 스킴을 선택한 뒤 게임을 시작해 직접 플레이하면, 전투를 돕고 체력을 채워 주고 말동무가 되어 주는 AI 동료가 곁을 따라다닙니다. [RPG 동료](https://war3ai.com/ko/docs/companion/)를 참고하세요. `g.map_data`로 맵의 커스텀 유닛 이름을 읽을 수 있고, [JASS 채널](https://war3ai.com/ko/docs/jass/)로 유닛 생성, 동맹 설정, 패널 띄우기 등을 할 수 있습니다…… 어떻게 놀지는 여러분이 정합니다. 월드를 바꾸는 조작은 싱글플레이 게임에서만 쓸 수 있고(멀티플레이에서는 동기화가 어긋남), 캔버스는 멀티플레이 게임에서도 안전합니다.
## 프로그래밍을 몰라도 쓸 수 있나요?
네. [빠른 시작](https://war3ai.com/ko/docs/quickstart/)에 따라 환경을 설치한 다음 [LLM으로 Bot 만들기](https://war3ai.com/ko/docs/ai-bot/)를 보세요. 여러분이 쉬운 말로 전략을 설명하면 LLM이 코드를 씁니다. 실행해 보고 문제가 있으면 오류 메시지나 게임에서 본 현상을 알려 주고 고치게 하면 됩니다.
## Python만 쓸 수 있나요?
SDK는 Python입니다. 런타임과 외부 프로그램 사이에는 공유 메모리 프로토콜([W3P](https://war3ai.com/ko/docs/protocol/)) 하나만 있으므로, Windows 공유 메모리를 읽고 쓸 수 있는 언어라면 무엇이든 연결할 수 있습니다. 더 간단한 방법은 [게이트웨이](https://war3ai.com/ko/docs/gateway/)(WebSocket / JSON)입니다. JS, C#, Go, Rust, 브라우저 페이지, 다른 컴퓨터의 프로그램 모두 같은 API를 호출할 수 있고, LLM Agent는 [MCP](https://war3ai.com/ko/docs/mcp/)로 바로 연결할 수 있습니다.
## 어떤 LLM이 가장 좋나요?
코드를 쓸 수 있는 주요 모델이라면 모두 됩니다. 핵심은 모델이 아니라 **올바른 자료를 주는 것**(매뉴얼 + `api.json` + 예제 하나)이며, API 카탈로그에 있는 메서드만 쓰도록 요구하는 것입니다. 게임 중 실시간 의사결정(참모, 유닛 대사)은 지연에 민감한데, 로컬 MoE 모델이 좋은 성능을 보입니다. [LLM 참모](https://war3ai.com/ko/docs/llm-coach/)와 [말풍선과 로컬 모델](https://war3ai.com/ko/docs/speech/)을 참고하세요.
## 게임이 느려지나요?
월드 상태 수집 1회는 게임 스레드에서 중앙값 0.5 ~ 0.9 ms(유닛 100 ~ 120개)이며, 50 ms마다 한 번 수행됩니다. 명령은 게임 스레드에서 건당 몇 마이크로초가 걸리고, 한 번 비울 때 4 ms의 시간 예산이 있어 다 처리하지 못한 것은 다음으로 넘기므로 게임을 붙잡지 않습니다. 게임에 대한 모든 호출에는 예외 보호가 있어서, Bot이 죽어도 그쪽만 멈출 뿐 게임까지 죽지는 않습니다.
## 여러 게임을 동시에 띄울 수 있나요?
네. `runtime/farm.py`이 다중 인스턴스 오케스트레이션을 맡으며, 인스턴스마다 번호가 하나씩 붙습니다. [Farsight 콘솔](https://war3ai.com/ko/docs/console/)에서 시작하고 중지합니다. 여러분의 Bot은 `--inst N`으로 지정한 인스턴스에 연결합니다.
## AI 두 개를 서로 싸우게 할 수 있나요?
같은 게임에서 `player` 채널을 두 개(`--player 0` / `--player 1`) 열면 AI 대 AI가 됩니다. 로컬 모드에서 공정성은 약속에 의존합니다. 심판, 시야 필터링, 소유권 검증을 갖춘 정식 대전은 [아레나](https://war3ai.com/ko/arena/)(P6 단계)에서 제공합니다.
## Mac / Linux를 지원하나요?
현재는 Windows 10 / 11만 지원합니다.
## 라이선스는 무엇인가요?
라이선스는 정식 버전과 함께 공개합니다. 서드파티 구성 요소는 각자의 라이선스를 유지합니다(예: MinHook은 BSD-2). AMAI는 자체 라이선스이므로 그 파생 데이터는 프로젝트와 함께 배포하지 않으며, 설치할 때 AMAI 공개 저장소에서 받아와 생성합니다.
## 문제가 생기면 어디에 알리나요?
정식 버전 출시 후 이슈 제보 채널을 열 예정입니다. 제보할 때는 인스턴스 번호, `python -m openwar3 status`의 출력, 재현 단계를 첨부해 주세요. 먼저 [디버깅과 성능](https://war3ai.com/ko/docs/debugging/)으로 해결되는지 확인해 보세요.
---
# 이용 범위
> 할 수 있는 일과 할 수 없는 일, 이 웹사이트의 방문 통계, 그리고 상표와 서드파티 라이선스 안내입니다. 이 프로젝트를 사용하면 이 범위를 준수하는 데 동의한 것으로 간주합니다.
## 할 수 있는 일
- **여러분이 합법적으로 소유한** 워크래프트 III 1.27 클라이언트에서 사용할 수 있습니다.
- 로컬, 오프라인, LAN 또는 직접 만든 게임에서 AI를 컴퓨터 상대나 다른 AI와 대전시킬 수 있습니다.
- 자신의 AI 대전을 연구, 교육, 오락, 방송에 활용할 수 있습니다.
- SDK, 레퍼런스 브레인, 예제, 도구를 바탕으로 2차 개발할 수 있으며, 각각의 라이선스를 준수해야 합니다.
## 할 수 없는 일
- **Battle.net 또는 안티치트가 있는 어떤 서버나 플랫폼에서도 사용할 수 없으며**, 안티치트가 활성화된 세션과 동시에 사용할 수도 없습니다.
- 사람 간 대전에서 부당한 이득을 얻는 데 사용할 수 없습니다.
- 블리자드의 게임 파일이나 거기서 추출한 데이터를 배포할 수 없습니다(이 프로젝트도 배포하지 않습니다. 게임 데이터는 사용자가 자신의 게임에서 추출합니다).
- 런타임의 이용 라이선스를 준수해야 합니다.
## 기술적으로 약속하는 것
- 디스크의 `Game.dll`이나 어떤 게임 파일도 수정하지 않습니다. 모든 변경은 실행 중에만 일어납니다.
- 다중 실행은 원본 `War3.exe` 런처를 그대로 복사해 이름만 바꿀 뿐입니다.
- 프로젝트에는 블리자드의 코드나 게임 파일이 전혀 포함되어 있지 않습니다.
## 사용자의 책임
지역마다 리버스 엔지니어링과 게임 수정에 관한 법률이 다릅니다. **사용자는 자신이 있는 지역에서 이 프로젝트를 사용하는 것이 합법인지 직접 확인해야 하며, 사용 결과에 대한 책임을 스스로 집니다.** 이 프로젝트는 "있는 그대로" 제공되며, 명시적이든 묵시적이든 어떠한 보증도 하지 않습니다.
## 이 웹사이트의 방문 통계
이 웹사이트(war3ai.com)는 Microsoft Clarity로 방문 현황을 집계합니다. 어떤 페이지를 봤는지, 어디서 왔는지, 얼마나 머물렀는지, 어디를 클릭하고 어디까지 스크롤했는지, 그리고 익명 세션 재생과 히트맵입니다. 이 데이터는 문서와 페이지를 개선하는 데만 사용합니다.
- 가입할 필요가 없으며, 이름이나 이메일 같은 신원 정보는 수집하지 않습니다. 입력란의 글자는 기본적으로 가려져 기록되지 않습니다.
- Clarity는 같은 방문자의 여러 차례 방문을 구분하기 위해 브라우저에 쿠키를 저장합니다. 데이터는 Microsoft가 처리하며, 자세한 내용은 [Microsoft 개인정보처리방침](https://privacy.microsoft.com/privacystatement)을 참고하세요.
- 집계를 원하지 않으면 이 사이트의 아무 주소 뒤에 `?stats=off`를 붙여 한 번 여세요. 그 브라우저에서는 이후로 집계하지 않습니다(`?stats=on`으로 되돌림). 브라우저의 추적 차단 기능으로 `clarity.ms`를 차단해도 되며, 웹사이트는 평소대로 이용할 수 있습니다.
로컬의 Farsight, SDK, 런타임에는 이런 통계 기능이 없습니다. Farsight는 두 가지 경우에만 war3ai.com에 연결합니다. 하나는 시작할 때와 그 뒤 6시간마다 버전 목록을 한 번 읽어 새 버전이 있는지 확인할 때, 다른 하나는 "피드백과 제안" 페이지에서 제출을 눌렀을 때입니다. 제출할 때는 여러분이 쓴 피드백과 로컬 식별 코드(시스템 ID에 솔트를 더해 해시한 값으로, 원래 값을 역산할 수 없으며 도배 방지에 쓰임)를 보냅니다. 진단 정보는 체크했을 때만 첨부되며, 보내기 전에 미리 볼 수 있습니다.
이 두 가지 요청이 war3ai.com에 도착하면 서버는 IP 주소, Cloudflare가 판단한 국가 또는 지역, 클라이언트 버전(User-Agent)을 기록합니다. 이는 남용을 방지하고 얼마나 많은 Farsight가 사용되고 있는지 집계하는 데 쓰입니다. 버전 확인 기록은 90일 후 자동으로 삭제되며, 피드백은 이 정보와 함께 유지관리자가 처리하고 삭제할 때까지 보관됩니다. 이 데이터는 프로젝트 유지관리자만 백엔드에서 볼 수 있으며, 다른 사람에게 제공되지 않습니다.
## 상표
Warcraft®, 워크래프트®는 Blizzard Entertainment, Inc.의 상표 또는 등록 상표입니다. War3AI / OpenWar3는 독립적인 커뮤니티 프로젝트로, Blizzard Entertainment와 관련이 없으며 승인이나 후원을 받지 않았습니다. 본문에 언급된 기타 제품명(Claude, GPT, Gemini, Qwen 등)은 각 소유자의 것이며, 호환성을 설명하기 위해서만 사용합니다.
## 서드파티 구성 요소와 데이터
| 구성 요소 / 데이터 | 라이선스 | 처리 방식 |
|---|---|---|
| MinHook | BSD-2-Clause | 런타임과 함께 사용하며 라이선스 고지를 유지 |
| AMAI | 자체 라이선스 | 프로젝트와 함께 배포하지 않음. `start.bat`이 배포할 때 AMAI 공개 저장소에서 받아온 뒤 도구로 레퍼런스 브레인용 데이터를 생성 |
| 게임 데이터(유닛, 스킬, 아이템 등) | 블리자드 | 프로젝트와 함께 배포하지 않음. 사용자가 자신의 게임에서 추출 |
| 공개 대회 리플레이에서 추출한 사실 데이터(건물 위치, 빌드 오더) | — | 사실 데이터만 포함하며, 레퍼런스 브레인에 사용 |
---
# API 카탈로그(api.json)
상태: verified = 내부 경로를 실게임에서 검증함; experimental = 새 API, 동작은 확인했고 항목별 실게임 검증 진행 중; inferred = 추정 / 실측 미완. 지연:푸시 스냅샷(공유 메모리를 읽으며 게임 스레드를 기다리지 않음(약 0.05 ms)); 고속 레인(약 1프레임: 게임 스레드에서 배치 실행); 제어 채널(20~40 ms(UI 관련 조작의 기존 경로)); 직접 쓰기(게임 스레드 대기열을 거치지 않음. 공유 메모리에 쓰거나(캔버스) 게임 창에 메시지를 보냄); 로컬 계산(순수 계산 또는 파일 읽기, 게임에 접근하지 않음)
## 관찰
상태를 읽기만 하며 게임을 바꾸지 않습니다. 대부분 푸시 스냅샷을 바로 읽으므로 대기가 없습니다.
- `snapshot(max_age: 'float' = 0.05)` [verified] [푸시 스냅샷] 맵 전체의 완전한 상태(WorldState): .units .players .items .clock .me. max_age초 안에 다시 호출하면 같은 스냅샷을 반환합니다.
⚠ 금광에 들어간 일꾼은 목록에 없습니다. 기본값은 맵 전체가 보이는 상태입니다(락스텝 모델이라 로컬에 모든 정보가 있음). Game(fair=True)일 때만 시야로 필터링합니다. (내부 메커니즘: W3P 월드 블록 Local\War3World_(런타임이 50 ms마다 푸시, seqlock))
- `last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'` [verified] [푸시 스냅샷] 마지막으로 본 적(또는 'creep' 크립, 또는 특정 플레이어 번호) 유닛: [(그때의 유닛 상태, 그때의 게임 시계, 지난 초)], 최신순.
죽는 것을 보면 목록에서 지웁니다. 공정 모드와 일반 모드 모두 "지금 아군이 볼 수 있는가"를 기준으로 기록합니다 — 플레이어 머릿속에 있는 바로 그 지도입니다:
정찰한 병력, 상대 영웅을 마지막으로 본 위치, 상대가 확장 기지를 언제 가져갔는지. max_age를 주면 그 게임 초 이내의 기록만 반환합니다. (내부 메커니즘: 푸시 스냅샷의 visibleTo (스냅샷을 갱신할 때마다 보이는 적/크립 유닛을 기록))
- `map()` [verified] [푸시 스냅샷] 이번 게임의 지형 테이블 MapInfo: .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (플레이 가능 영역) .starts (시작 지점) .cells (bit0 이동 불가, bit1 건설 불가).
게임 시작 후 계산이 끝나기까지 몇 초 걸리며, 그 전에는 None을 반환합니다. 나무는 포함되지 않습니다(trees() 사용). (내부 메커니즘: W3P 맵 블록 Local\War3Map_(게임 시작 후 런타임이 나눠서 계산, IsTerrainPathable 이동/건설))
- `me() -> 'int | None'` [verified] [푸시 스냅샷] 내가 몇 번 플레이어인지(0~11). (내부 메커니즘: 월드 블록 헤더)
- `resources(player: 'int | None' = None) -> 'dict | None'` [verified] [푸시 스냅샷] {'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}. player 기본값은 아군이며, 모든 플레이어를 읽을 수 있습니다.
읽지 못하면 None을 반환합니다. 0으로 취급하지 마세요. (내부 메커니즘: 월드 블록 players[16])
- `players() -> 'list'` [verified] [푸시 스냅샷] 16개 플레이어 슬롯 전체: Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known). (내부 메커니즘: 월드 블록 players[16])
- `units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'` [verified] [푸시 스냅샷] 소유자/종류로 유닛을 필터링합니다. owner: 'me' / 'enemy' / 'creep' / 'all' / 플레이어 번호. types: 4자 코드 집합. (내부 메커니즘: 월드 블록 units[])
- `unit(handle) -> 'object | None'` [verified] [푸시 스냅샷] 핸들 쌍 (lo, hi)로 유닛을 찾습니다(오더 대상, 작업 대상, 이벤트가 주는 값은 모두 핸들 쌍). (내부 메커니즘: 월드 블록 by_handle)
- `is_building(u) -> 'bool'` [verified] [푸시 스냅샷] 건물인지 여부(타워 포함). 유닛 테이블의 이동 속도가 0인지로 판정합니다. 언데드 본진 건물은 점유 면적이 0이므로 점유 면적으로 판정하지 마세요. (내부 메커니즘: 스냅샷 + units.json (spd==0 = 건물))
- `my_workers() -> 'list'` [verified] [푸시 스냅샷] 아군 일꾼(농부 / Peon / 시종 / 위습). (내부 메커니즘: 푸시 스냅샷)
- `idle_workers() -> 'list'` [verified] [푸시 스냅샷] 할 일이 없는 일꾼: 오더도 없고 작업도 없는 일꾼(이번 틱에 방금 일을 맡긴 일꾼은 제외).
⚠ 작업 중인 일꾼에게 채집 명령을 다시 내리면 채집 주기가 끊깁니다(수입이 0이 됨). (내부 메커니즘: 푸시 스냅샷(오더 슬롯 + 작업 슬롯))
- `my_heroes() -> 'list'` [verified] [푸시 스냅샷] 살아 있는 아군 영웅(죽은 영웅은 제단의 부활 목록에 있음, revive 참고). (내부 메커니즘: 푸시 스냅샷)
- `my_army() -> 'list'` [verified] [푸시 스냅샷] 아군 전투 유닛: 일꾼도 건물도 아닌 유닛. (내부 메커니즘: 푸시 스냅샷 + units.json)
- `my_buildings(types=None) -> 'list'` [verified] [푸시 스냅샷] 아군 건물(타워, 건설 중인 기초 포함). types로 특정 종류만 고를 수 있습니다. 예: {'hbar'}. (내부 메커니즘: 푸시 스냅샷)
- `is_constructing(worker) -> 'bool'` [verified] [푸시 스냅샷] 이 일꾼이 건물을 짓고 있는지(지으러 가는 중 / 수리를 돕는 중 포함, 이번 틱에 방금 맡긴 것도 포함). 건설 일꾼을 고를 때 이 일꾼은 건너뛰어야 합니다. 그렇지 않으면 이전 기초 공사가 멈춥니다. (내부 메커니즘: 푸시 스냅샷(오더 = 건물 4자 코드, 또는 건설/수리 오더))
- `under_construction(building) -> 'bool'` [verified] [푸시 스냅샷] 이 건물이 아직 완성되지 않았는지(체력이 가득 차지 않음). ⚠ 공격받아 손상된 건물도 체력이 가득 차 있지 않습니다 — 초반 판단에는 충분하지만, 교전이 시작된 뒤에는 시간과 함께 판단하세요. (내부 메커니즘: 푸시 스냅샷(기초의 체력이 아주 낮은 값에서 최대치까지 오름))
- `gold_mines() -> 'list'` [verified] [푸시 스냅샷] 맵 위의 금광. ⚠ 나이트 엘프가 휘감은(Entangle) 금광과 중립 금광은 같은 좌표에 유닛이 하나씩 있습니다. 채집은 자기 쪽 금광으로 보내야 합니다. (내부 메커니즘: 푸시 스냅샷(ngol/egol/ugol))
- `enemies(fighters_only: 'bool' = False) -> 'list'` [verified] [푸시 스냅샷] 적 플레이어의 유닛(크립 제외). fighters_only: 일꾼과 건물을 제외합니다. (내부 메커니즘: 푸시 스냅샷)
- `creeps() -> 'list'` [verified] [푸시 스냅샷] 크립(중립 적대). ⚠ 밤에는 시야가 짧아져, 먼 캠프가 안개 속으로 들어가면 그 대상에게 내리는 명령이 거부됩니다(원인 코드 1001). (내부 메커니즘: 푸시 스냅샷(owner 12 = 중립 적대))
- `life_mana(u) -> 'dict | None'` [verified] [푸시 스냅샷] {'hp','hp_max','mana','mana_max'}(부동소수점, 엔진 원값). u는 스냅샷에서 얻은 유닛이면 됩니다(최신 값으로 바꿔 읽음). (내부 메커니즘: 월드 블록 유닛 hp/hpMax/mana/manaMax)
- `hero_info(hero) -> 'dict | None'` [verified] [푸시 스냅샷] {'level','xp','skill_points'}. (내부 메커니즘: 월드 블록 유닛 level/xp/skillPoints)
- `abilities(u) -> 'list'` [verified] [푸시 스냅샷] [{code, level, cooldown, flags}]. 버프는 buffs(u)에 있습니다. "상세 정보"가 있는 유닛에만 있습니다(영웅 > 플레이어 유닛 > 크립, 최대 256개). (내부 메커니즘: 월드 블록 상세: 스킬(코드 / 레벨 / 플래그 / 남은 쿨다운))
- `buffs(u) -> 'list'` [verified] [푸시 스냅샷] 유닛에 걸린 버프 코드(예: 'BHds' Divine Shield, 'Bslo' 감속). 코드별 효과는 data/game/buffs.json 참고. (내부 메커니즘: 월드 블록 상세: B로 시작하는 스킬 객체)
- `cooldown(u, ability: 'str') -> 'float | None'` [verified] [푸시 스냅샷] 이 스킬의 남은 쿨다운(게임 초). 0 = 사용 가능. 이 스킬이 없으면(또는 이 유닛에 상세 정보가 없으면) None을 반환합니다. (내부 메커니즘: 월드 블록 상세: 스킬 남은 쿨다운(스킬 타이머))
- `inventory(hero) -> 'list | None'` [verified] [푸시 스냅샷] 6칸 아이템의 4자 코드(빈칸은 None). 인벤토리가 없으면 None을 반환합니다. (내부 메커니즘: 월드 블록 상세: 인벤토리 6칸)
- `current_order(u) -> 'dict | None'` [verified] [푸시 스냅샷] {'order','target','x','y'}: 유닛이 지금 수행 중인 오더(order는 0x000D00xx 또는 건물 4자 코드, 0 = 대기).
target은 핸들 쌍이며, g.unit(target)으로 유닛으로 바꿉니다. (내부 메커니즘: 월드 블록 유닛 오더 / 오더 대상 / 오더 대상 지점)
- `current_target(u)` [verified] [푸시 스냅샷] 유닛이 **실제로 공격하거나 쫓고 있는** 유닛(없으면 None).
⚠ 공격 명령을 내리면 오더 슬롯은 금방 비고 공격은 작업(task)에 걸립니다 — "누구를 공격 중인지"는 current_order가 아니라 이것으로 판단하세요. (내부 메커니즘: 월드 블록 유닛 작업 대상)
- `clock() -> 'float | None'` [verified] [푸시 스냅샷] 엔진 게임 시계(게임 초, 로딩 중에는 0). 배속에서는 실제 시간보다 빠르게 흐릅니다. (내부 메커니즘: 월드 블록 헤더 clockMs (엔진 게임 시계))
- `production(building)` [verified] [푸시 스냅샷] 이 건물이 지금 무엇을 만들고 있는지: Production(kind, queue, duration, elapsed, blocked, progress, remaining…). 아무것도 안 하면 None을 반환합니다.
kind 'queue'(훈련/연구/영웅, queue는 최대 7칸, [0]이 진행 중) / 'construction'(건설 중) / 'upgrade'(본진 건물/타워 업그레이드);
blocked = 대기열은 있지만 시작하지 못함(대개 인구 부족 — 농장을 지을 때입니다); progress 0..1.
상대 건물도 볼 수 있습니다(공정 모드에서는 보이는 건물만). (내부 메커니즘: 월드 블록 생산 테이블(Aque/ABnP/AUnP 스킬 객체 + 런타임이 추적한 경과 시간, 실측 오차 < 0.2 게임 초))
- `queue(building) -> 'list'` [verified] [푸시 스냅샷] 훈련/연구 대기열의 4자 코드([0]이 진행 중). 대기 중이거나 생산 건물이 아니면 []. (내부 메커니즘: 월드 블록 생산 테이블)
- `all_production(owner: 'str | int' = 'me') -> 'list'` [verified] [푸시 스냅샷] 진행 중인 모든 생산 [(건물, Production)]. owner는 units()와 같습니다: 'me' / 'enemy' / 플레이어 번호 / 'all'.
프로의 활용법: 상대가 어떤 유닛을 뽑는지, 어떤 기술을 연구하는지, 언제 테크 업을 하는지 봅니다(건물을 정찰했을 때). (내부 메커니즘: 월드 블록 생산 테이블)
- `path_distance(a, b) -> 'float | None'` [verified] [푸시 스냅샷] 지상 유닛이 a에서 b까지 걸어가는 거리(a, b는 유닛 또는 (x,y)). 갈 수 없으면 None. 섬 맵에서 "이 크립 캠프/확장 기지에 지상으로 갈 수 있는가"를 판단할 때 쓰세요.
직선 거리보다 믿을 만합니다(숲, 절벽, 건물을 돌아감). 정밀도는 한 칸 128이며, 한 칸보다 좁은 틈은 막힌 것으로 판정합니다. (내부 메커니즘: 맵 블록(엔진 IsTerrainPathable) + 트리 블록 + 건물 점유 면적, SDK 측 A*(128당 한 칸))
- `reachable(a, b) -> 'bool | None'` [verified] [푸시 스냅샷] 지상으로 갈 수 있는지 여부(맵 블록 계산 전 = None). (내부 메커니즘: 위와 같음)
- `walk_path(a, b) -> 'list | None'` [verified] [푸시 스냅샷] 경로의 꺾이는 지점 [(x,y)...](마지막 점이 b). path(units, 지점 목록)와 함께 쓰면 부대가 이 경로를 따라 이동합니다(타워를 피하고 샛길로). (내부 메커니즘: 위와 같음)
- `upkeep(player: 'int | None' = None) -> 'dict | None'` [inferred] [푸시 스냅샷] 유지비 단계: {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': 다음 단계의 인구(없으면 None)}.
프로의 상식: 3티어 테크 업이나 공격/방어 업그레이드 중에는 인구 50에서 멈추고, 결전 직전에야 80까지 늘립니다. (내부 메커니즘: 1.27 고정 규칙: 인구 0~50은 유지비 없음, 51~80은 수입 ×0.7, 81~100은 ×0.4)
- `xp_to_next(hero) -> 'int | None'` [verified] [푸시 스냅샷] 영웅이 다음 레벨까지 필요한 경험치(10레벨 = 0). (내부 메커니즘: 월드 블록 level/xp + MiscGame NeedHeroXP 공식)
- `creep_camps(link: 'float' = 600.0) -> 'list'` [verified] [푸시 스냅샷] 필드의 (보이는) 크립을 캠프로 묶습니다: [{'x','y','units','level','hp','max_level'}], 아군 본진에서 가까운 순.
level = 캠프 총 레벨(사냥 난이도를 흔히 재는 기준), hp = 총 체력. time_to_kill / path_distance와 함께 사냥할 곳을 고르세요. (내부 메커니즘: 푸시 스냅샷(크립을 600 거리 기준으로 한 무리로 묶음) + units.json 레벨)
- `buff_info(code: 'str') -> 'dict | None'` [verified] [로컬 계산] 버프 코드가 무엇인지: {'ability','effect','dur','hero_dur','targets'}(예: 'Bslo' -> 감속). 한 코드에 여러 행이 있으면 첫 행을 반환합니다. (내부 메커니즘: data/game/buffs.json (AbilityData.slk의 BuffID -> 스킬/효과/지속 시간))
- `stats(u, player: 'int | None' = None)` [verified] [푸시 스냅샷] 유닛의 전투 속성 combat.UnitStats: 체력/마나 최대치, 방어력(공격/방어 업그레이드, 영웅 민첩 포함), 방어 타입, 이동 속도, 낮/밤 시야,
무기(공격 가능 대상, 사거리, 공격 간격, 피해량 범위, 공격 타입, 스플래시). u는 유닛(소유자의 기술과 영웅 레벨을 자동 적용) 또는 4자 코드(player 기본값은 아군).
.dps_vs(상대) / .hits_to_kill(상대) / combat.time_to_kill(무리, 상대)와 함께 쓰세요. ⚠ 아이템, 오라, 버프는 반영하지 않습니다. (내부 메커니즘: 데이터 테이블(UnitBalance/UnitWeapons/UpgradeData/MiscGame) + 실시간 기술 레벨 + 영웅 레벨)
- `time_to_kill(attackers, target) -> 'float | None'` [verified] [푸시 스냅샷] 이 유닛 무리가 함께 target을 공격해 죽이는 데 걸리는 게임 초(target의 현재 체력 기준. 상성, 방어력, 공격/방어 업그레이드는 반영하고 무빙, 스플래시, 치유는 반영하지 않음).
프로의 활용법: 점사는 가장 가까운 적이 아니라 "가장 빨리 죽일 수 있는" 적(time_to_kill이 가장 작은 적)부터. 공격할 수 없으면 None. (내부 메커니즘: stats() + 실시간 체력)
- `time_of_day() -> 'float | None'` [verified] [푸시 스냅샷] 게임 내 시각(시, 0~24). 시작은 오전 8시이며, 하루 = 480 게임 초(낮과 밤 각 240초, 낮밤 진행 속도에 따라 조정).
읽지 못하면(구버전 런타임 / 게임 중이 아님) None을 반환합니다. (내부 메커니즘: 월드 블록 확장 영역: GetFloatGameState(GAME_STATE_TIME_OF_DAY))
- `is_night() -> 'bool | None'` [verified] [푸시 스냅샷] 지금이 밤인지(18:00~6:00). 프로의 운영: 밤에는 크립이 잠들고(먼저 공격해도 포위당하지 않음), 모든 유닛의 시야가 짧아지며(기습하기 좋은 때),
나이트 엘프의 보초와 유닛은 밤에 나무 곁에서 은신합니다. 읽지 못하면 None. (내부 메커니즘: 월드 블록 확장 영역(6~18시가 낮))
- `seconds_until(hour: 'float') -> 'float | None'` [verified] [푸시 스냅샷] 게임 내 시각 hour시까지 남은 게임 초(예: seconds_until(18) = 해가 지기까지 남은 시간, 밤 사냥을 계획할 때 사용). (내부 메커니즘: 월드 블록 확장 영역 + 하루 480초(실측 1시간 = 20 게임 초))
- `items_on_ground() -> 'list'` [verified] [푸시 스냅샷] 바닥에 있는 아이템 [Item(addr, handle_lo, handle_hi, type, x, y, life)]. 주워 가거나 사용되면 item.removed 이벤트가 발생합니다. (내부 메커니즘: 월드 블록 items[] (바닥에 있는 것만: 소지자 핸들이 모두 FF))
- `trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'` [verified] [푸시 스냅샷] 살아 있는 나무(DestructableData에서 targType에 tree가 포함된 것). (x,y)를 주면 가까운 순으로, 최대 limit그루.
각각은 Tree(addr, handle_lo, handle_hi, type, x, y, life)이며, 그대로 gather에 넘겨 벌목할 수 있습니다. (내부 메커니즘: 트리 블록 Local\War3Trees_(2초마다 갱신))
- `events() -> 'list'` [verified] [푸시 스냅샷] 마지막 호출 이후에 일어난 일: unit.appeared / unit.died / unit.removed / unit.damaged / order.changed /
hero.levelup / owner.changed / item.appeared / item.removed / game.started (발행 간 비교로 얻음, 정밀도 = 발행 주기 50 ms),
그리고 엔진 수준의 damage / killed (런타임이 게임 스레드에서 발생 즉시 기록, **타격마다** 발생):
damage: handle = 맞은 유닛, .source_addr = 때린 유닛(snapshot().unit_by_addr로 유닛으로 변환), .value = 실제로 깎인 체력,
.raw_damage = 방어력 적용 전 피해, .attack_type(normal/pierce/siege/magic/chaos/hero/spell), .damage_type
killed: 이 타격으로 죽음, .source_addr = 처치한 유닛
그리고 런타임이 생산 테이블을 추적해 얻는 production.done (정밀도 = 발행 주기): 유닛 = 건물, .done_code = 완료된 4자 코드,
.done_kind = 'training'(유닛/영웅/부활) / 'research' / 'construction'(건물 완공) / 'upgrade'(테크 업/타워 업그레이드), .value = 걸린 게임 초
09-25 보강:
spell.cast: 유닛 = 시전자, .spell 스킬 4자 코드, b 레벨, value 쿨다운 초, x,y 시전 지점(스킬 쿨다운이 시작될 때 인식, 정밀도 = 발행 주기)
player.left: .player 나가거나 패배 판정으로 제거된 플레이어 번호; game.ended: 게임에서 나감
selection.changed: 로컬 플레이어의 선택이 바뀜(g.selection()으로 유닛을 가져옴)
message: 화면 메시지 영역의 한 줄(게임 안내, 채팅, 시스템): .text 전체 텍스트, .frame 메시지 영역 번호,
.chat = {'channel', 'sender', 'text'}(채팅일 때. 플레이어가 채팅창에 입력한 내용은 여기서 읽음)
ui.click / ui.hover / hotkey / mouse.world: UI와 입력(g.ui), .key는 캔버스 key / 단축키 표기
각 항목은 Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra)입니다.
공정 모드(fair=True)에서는 다음만 제공합니다: 자기 유닛의 이벤트, 지금 보이는(또는 1초 전까지 보였던) 유닛의 이벤트, 아군이 받거나 가한 피해,
그리고 로컬 UI / 메시지 / 게임 진행 관련 이벤트. (내부 메커니즘: 이벤트 링 Local\War3Events_(발행 간 비교 + 런타임이 포착한 피해 이벤트))
- `selection() -> 'list'` [verified] [푸시 스냅샷] 로컬 플레이어가 지금 선택한 유닛(주 유닛이 맨 앞, 최대 12개). 선택이 바뀌면 selection.changed 이벤트가 발생합니다. (내부 메커니즘: W3P 월드 블록 확장 영역 selAddrs(런타임이 발행할 때마다 로컬 플레이어의 선택을 함께 담음))
- `messages() -> 'list'` [verified] [푸시 스냅샷] 마지막 호출 이후 화면 메시지 영역에 새로 나온 메시지: [{'text', 'frame', 'repeat', 'seq', 'game_ms'}].
게임 안내("농장이 더 필요합니다", "그곳에는 건설할 수 없습니다"), 채팅, 시스템 메시지가 모두 여기에 있으며, frame으로 어느 메시지 영역인지 구분합니다.
이벤트 스트림의 message 이벤트와 같은 메시지입니다(커서는 각자 따로). (내부 메커니즘: 공유 메모리 Local\War3Msgs_(런타임이 포착한 화면 메시지))
- `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [고속 레인] 연구 레벨 / 완성된 건물 수(업그레이드 체인 포함: 성도 htow로 셈). player 기본값은 아군이며, 모든 플레이어를 조회할 수 있습니다. (내부 메커니즘: W3P 쿼리 q_tech (엔진의 플레이어 기술 카운트))
- `can_do(u, code: 'str') -> 'int | None'` [verified] [고속 레인] 엔진의 실행 가능성 판정: 0/220 가능, 3 인구 부족, 8 금 부족, 9 목재 부족, 32 대기열 가득 참, 183 선행 조건 부족, 185 제단에서 부활 중, 221 해당 항목 없음/건설 중.
⚠ 일꾼의 건물 건설에는 항상 221이므로 건설 위치 판정에는 쓸 수 없습니다(build_near 사용). (내부 메커니즘: W3P 쿼리 q_feasible (엔진 실행 가능성 검사))
- `can_do_many(pairs) -> 'list'` [verified] [고속 레인] can_do를 한 번에 여러 개 묻습니다: pairs = [(유닛, 4자 코드), ...], 같은 순서의 판정 코드 목록을 반환합니다(조회하지 못한 것은 None).
한 틱에 무엇을 짓고 뽑을지 계획할 때 먼저 한꺼번에 물어보면 can_do를 하나씩 부르는 것보다 N배 빠릅니다(레퍼런스 브레인 09-23: 건설 계획 76 -> 25 ms). (내부 메커니즘: W3P 쿼리 q_feasible × N, 한 배치로 제출)
- `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [고속 레인] 기술/건물 카운트를 한 번에 여러 개 조회합니다: {4자 코드: 개수 또는 None}. (내부 메커니즘: W3P 쿼리 q_tech × N, 한 배치로 제출)
- `visible(x: 'float', y: 'float') -> 'bool | None'` [verified] [고속 레인] 이 지점이 지금 아군에게 보이는지(안개/검은 영역이 아님). 공정 모드 봇은 보이는 적만 사용해야 합니다. (내부 메커니즘: W3P 쿼리 q_visible (보임 / 안개 / 검은 영역))
- `gold_left(mine) -> 'int | None'` [inferred] [고속 레인] 금광에 남은 금. (내부 메커니즘: W3P 쿼리 q_mine_gold (엔진의 금광 잔량))
- `enemy_ai_plan(enemy_unit) -> 'dict | None'` [verified] [고속 레인] 컴퓨터 AI의 대장: 병력을 이끌고 어디로 가려는지(출발 전부터 기지의 어디를 칠지 알 수 있음). 컴퓨터 상대에게만 유효하며, 대장을 따르지 않으면 None을 반환합니다. (내부 메커니즘: W3P 쿼리 q_captain (적 병력이 따르는 컴퓨터 대장))
- `order_of(u) -> 'int | None'` [verified] [푸시 스냅샷] 유닛의 현재 오더. **이번 틱에 방금 내린 것도 포함**합니다(스냅샷이 아직 따라잡지 못했으면 회신의 새 오더를 사용).
⚠ 09-23 실전: hello_bot이 농부를 농장 건설에 막 보냈는데, 같은 틱에 rush_bot이 스냅샷에서 그 농부를 "대기 중"으로 보고 병영 건설에 또 보내서, 농장이 번번이 중간에 포기되었습니다.
"놀고 있는/건물을 짓고 있지 않은" 유닛을 고를 때는 u.order 대신 이것을 쓰세요. (내부 메커니즘: 스냅샷 오더 + 이 프로세스에서 방금 수락된 명령(회신))
- `can_afford(code: 'str') -> 'bool'` [verified] [푸시 스냅샷] 지금 금/목재로 code (유닛, 건물)를 살 수 있는지(units.json의 가격 기준). 가격표에 없는 것은 모두 살 수 있는 것으로 취급합니다.
⚠ 테크 업 4자 코드는 표에 누적 가격으로 되어 있어 여기서는 보수적으로 판단됩니다. 최종 판단은 엔진의 회신을 따르세요. (내부 메커니즘: 푸시 스냅샷의 아군 자원 + units.json의 가격)
- `map_data()` [verified] [로컬 계산] 지금 플레이 중인 맵의 데이터(openwar3.mapdata.MapData): name_of('HC07') 커스텀 유닛/아이템/스킬의 이름, hero_names, tooltip.
RPG 맵의 유닛은 대부분 맵이 직접 만든 것이라 내장 이름 테이블에 없습니다. 런처로 시작한 게임이 아니면(맵 파일을 찾을 수 없으면) None을 반환합니다. (내부 메커니즘: 맵 파일(런처 --map 경로): w3u/w3t/w3a + wts, 보호된 맵은 맵 안의 TXT를 읽음)
## 명령
유닛에게 일을 시킵니다. 약 1프레임 만에 반영되며, 모두 회신이 있습니다.
- `batch() -> 'Batch'` [verified] [고속 레인] 한 틱의 명령을 한 배치로 묶습니다:
with g.batch() as b:
g.attack(archers, target) # Pending 반환, 블록이 끝난 뒤 회신이 됨
g.move(wounded, *home)
g.cast(hero, "thunderclap")
print(b.sent, b.wait_ms, [r.reason for r in b.receipts])
명령을 하나씩 보내면 매번 게임 스레드가 한 번 처리할 때까지 기다려야 합니다(약 10 ms). 배치는 한 번만 기다립니다 — 레퍼런스 브레인 09-23은 이것으로 한 라운드를 48 -> 26 ms로 줄였습니다.
* 중재는 여전히 명령별로 거칩니다(점유된 유닛은 즉시 held 회신을 받고 배치에 들어가지 않음);
* 블록 안의 명령은 Pending을 반환합니다: 블록이 끝나기 전에 .ok를 읽으면 예외가 발생하고(회신이 아직 없음), 블록이 끝난 뒤에는 Receipt처럼 씁니다;
* 블록 안에서 예외가 발생하면 = 배치 전체 취소(status 97 cancelled), 점유했던 유닛은 반환됩니다;
* 쿼리(can_do / tech / visible …)와 build_near, buy는 배치에 들어가지 않고 그 자리에서 바로 묻습니다 — 결과를 즉시 써야 하기 때문입니다;
여러 개를 한 번에 물으려면 can_do_many / tech_many를 쓰세요;
* 중첩된 with g.batch()는 가장 바깥 배치에 합쳐집니다. 16개를 넘으면 런타임이 자동으로 여러 구간으로 나눕니다(구간마다 한 번 대기). (내부 메커니즘: 블록 안의 명령을 한 배치로 모아 블록이 끝날 때 한 번에 제출(같은 프레임에 실행, 게임 스레드를 한 번만 기다림))
- `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [고속 레인] (x,y)로 이동하며, 가는 길에 공격하지 않습니다(후퇴할 때 사용). 유닛 하나 또는 목록을 넘길 수 있습니다(같은 프레임에 함께 명령).
queue='after': 지금 하는 일을 끝낸 뒤 이동(현재 오더 뒤에 삽입). 회신 values[0] = 명령 후 이 유닛에 쌓인 오더 수(진행 중인 것 포함). (내부 메커니즘: W3P point: move (extra 비트 = 대기열 방식))
- `attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [고속 레인] 공격 이동(A 지면 클릭): 가는 길에 적을 만나면 공격합니다. queue는 move와 같습니다. (내부 메커니즘: W3P point: 지점 대상 attack)
- `attack(units, target, force: 'bool' = False, queue: 'str | None' = None)` [verified] [고속 레인] target을 공격합니다. 기본은 우클릭입니다(적에게 = 이 대상 하나를 공격, 09-23 실측 결과 오더 대상/작업 대상 모두 그 대상).
⚠ 대상은 반드시 시야 안에 있어야 하며, 보이지 않으면 거부됩니다(원인 코드 1001).
force=True는 공격 오더 0x0F를 사용합니다(아군/중립 동물을 공격할 때 필요) — 실측 결과 공격 오더만 바꾸고 대상을 기억하지 않아,
근처의 다른 적을 공격하러 갑니다. 특정 대상을 공격할 때는 쓰지 마세요. (내부 메커니즘: W3P target: 대상 명령(우클릭 smart))
- `stop(units)` [verified] [고속 레인] 하던 일을 모두 멈춥니다(오더 ID 0x000D0004). 대기 중인 오더도 지웁니다. (내부 메커니즘: W3P immediate: stop)
- `hold(units, queue: 'str | None' = None)` [verified] [고속 레인] 위치 사수(쫓아가지 않고 사거리 안의 적만 공격). (내부 메커니즘: W3P immediate: holdposition)
- `patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)` [inferred] [고속 레인] 현재 위치와 (x,y) 사이를 순찰합니다. (내부 메커니즘: W3P point: patrol)
- `attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [고속 레인] 지면 공격: 포가 한 지점에 포격합니다(은신 유닛, 숲 뒤의 적 공격, 길목 봉쇄). 지면 공격이 가능한 유닛만 받아들입니다. (내부 메커니즘: W3P point: attackground (공성 유닛 / 박격포 / 투석기))
- `cancel(building)` [verified] [고속 레인] 취소: 훈련/연구 대기열의 마지막 칸(환불), 건설 중인 건물(75% 환불), 업그레이드 중인 본진 건물. (내부 메커니즘: W3P immediate: cancel)
- `path(units, points, attack: 'bool' = False)` [verified] [고속 레인] 일련의 지점을 순서대로 지나갑니다(Shift 연속 클릭: 경유지, 타워 우회, 정찰 경로). attack=True면 각 구간이 공격 이동입니다.
한 번에 제출하며, 회신은 지점마다 하나씩(points 순서)입니다. (내부 메커니즘: 한 배치: 첫 구간은 즉시 실행, 나머지는 역순으로 queue='after'로 삽입(엔진은 현재 오더 뒤 삽입만 지원))
- `gather(workers, target, queue: 'str | None' = None)` [verified] [고속 레인] 금 채집/벌목(target은 금광 또는 trees()의 나무). ⚠ 놀고 있는 일꾼(idle_workers)에게만 맡기세요: 작업 중인 일꾼에게 다시 명령하면 채집 주기가 끊깁니다.
프로의 활용법: 건물을 다 지은 뒤 채광으로 복귀 = build(...) 다음에 gather(worker, mine, queue='after'). (내부 메커니즘: W3P target: harvest (금광 또는 나무))
- `repair(workers, building, queue: 'str | None' = None)` [verified] [고속 레인] 수리 / 건설 돕기(휴먼과 오크의 건설 현장은 짓는 일꾼이 없으면 공사가 멈춤). (내부 메커니즘: W3P target: repair)
- `build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [고속 레인] 일꾼이 (x,y)에 code를 짓게 합니다(좌표는 32 단위로 정렬). 회신 수락 = 일꾼의 오더가 이미 이 건물(또는 착공 오더)이 됨.
queue='after'일 때 = 일꾼의 오더 대기열에 들어감(회신 values[0]은 대기 수).
⚠ 수락 ≠ 건설 성공: 숲 속 지점도 엔진은 즉시 수락하고, 일꾼이 도착해서야 실패합니다(09-23 실측). 돈이 다른 곳에 쓰여도 기초가 생기지 않습니다.
어디에 지을 수 있는지 모르면 build_near를 쓰세요(결과를 추적하고 실패한 지점을 블랙리스트에 올림). 여러 채를 연달아 지을 때는 build_queue. (내부 메커니즘: W3P build: 건설 오더, 같은 프레임에 일꾼 오더를 다시 읽어 확인)
- `build_queue(worker, plan)` [verified] [고속 레인] 일꾼 한 명이 여러 건물을 순서대로 연달아 짓습니다(Shift 연속 건설): plan = [(4자 코드, x, y), ...]. 한 번에 제출하며, 회신은 plan 순서입니다.
⚠ 비용은 착공할 때 차감됩니다(대기열에 넣을 때는 차감 안 됨) — 3채를 예약했는데 돈이 1채분뿐이면, 나머지 두 채는 일꾼이 도착했을 때 실패합니다. (내부 메커니즘: 한 배치: 첫 건물은 즉시, 나머지는 역순으로 queue='after')
- `build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)` [verified] [고속 레인] (x,y) 주변에서 가까운 곳부터 차례로 지을 수 있는 지점을 찾아 code를 짓습니다. **블로킹하지 않으므로** 매 틱 호출해도 됩니다:
* 이 건물의 시도가 아직 진행 중(일꾼이 이동 중) -> 그 지점을 반환하고 명령을 반복하지 않음;
* 이전 시도가 성공(기초가 생김) -> 이번에는 필요하면 새 지점을 찾음;
* 이전 시도가 실패(일꾼이 도착해서야 지을 수 없음을 알고, 엔진이 오더를 취소했으며 기초가 없음) -> 그 지점을 45초간 블랙리스트에 올리고 다음 지점으로;
* 돈이 부족 -> 바로 None 반환(시도하지 않고 블랙리스트에도 올리지 않음); 모든 지점을 시도했으면 None 반환.
⚠ 추적이 필요한 이유: 09-23 실전에서 숲 속 지점을 엔진이 **즉시 수락**하고 일꾼이 도착해서야 실패했습니다(같은 프레임의 회신으로는 판별 불가);
게다가 엔진의 배치 위치 검사는 일꾼의 건물 건설에 대해 항상 221을 반환하므로, 먼저 "조회"하고 지을 수도 없습니다. 명백히 점유된 지점(본진 건물 한가운데)만 즉시 거부됩니다. (내부 메커니즘: 지점마다 build + 추적(기초 생성 = 성공, 일꾼이 오더를 포기했는데 기초가 없음 = 그 지점 블랙리스트))
- `train(building, code: 'str')` [verified] [고속 레인] 유닛 훈련 / 기술 연구 / 본진 건물 업그레이드(테크 업 = 본진 건물 자체에 목표 본진 건물의 4자 코드를 명령, 예: 'hkee').
거부되면 회신의 reason에 이유가 적힙니다(인구 부족, 금 부족, 목재 부족, 대기열 가득 참, 선행 조건 부족……). (내부 메커니즘: W3P immediate: 4자 코드, 거부 시 실행 가능성 원인 코드 포함)
- `learn(hero, ability: 'str')` [verified] [고속 레인] 영웅이 스킬을 배웁니다(4자 코드, 예: 'AHbz' 눈보라). (내부 메커니즘: W3P learn: 스킬 포인트가 줄어야 습득으로 인정)
- `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [고속 레인] 스킬을 사용합니다. spell은 오더 문자열('thunderbolt' 폭풍 망치, 'blizzard', 'holybolt' Holy Light…, data/order-ids.txt 참고) 또는 오더 ID.
target을 주면 = 유닛 대상, x,y를 주면 = 지면 대상, 둘 다 없으면 = 대상 없음(Thunder Clap, Divine Shield, 물의 정령 소환).
회신 수락은 엔진이 받아들였다는 뜻일 뿐입니다. 실제로 시전됐는지는 cooldown()이 쿨다운에 들어갔는지, buffs()에 버프가 나타났는지로 확인하세요. (내부 메커니즘: W3P target / point / immediate (인자에 따라 선택))
- `rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)` [verified] [고속 레인] 집결 지점을 설정합니다(지점, 또는 유닛/금광 대상). (내부 메커니즘: W3P rally)
- `revive(altar, hero=None)` [verified] [고속 레인] 제단에서 죽은 영웅을 부활시킵니다(hero를 주지 않으면 목록의 첫 번째를 부활).
흔한 거부 원인(회신 reason에 적힘): 인구 부족(영웅도 인구를 차지함), 돈 부족, 죽은 지 얼마 안 됨(사망 후 약 3 게임 초가 지나야 부활 가능),
이미 부활 진행 중(수락될 때 엔진이 그 슬롯을 즉시 비움). (내부 메커니즘: W3P revive: 사망 영웅 목록 -> 제단이 죽은 영웅에게 부활 시전)
- `pick_up(hero, item)` [verified] [고속 레인] 영웅이 바닥의 아이템을 주우러 갑니다(item은 items_on_ground에서). 주우면 인벤토리에 나타나고, 바닥 쪽에는 item.removed 이벤트가 발생합니다. (내부 메커니즘: W3P target: 아이템 우클릭)
- `use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [고속 레인] 인벤토리 slot번째 칸(0~5)의 아이템을 사용합니다. 대상 유닛이나 대상 지점을 지정할 수 있습니다.
⚠ 지점 대상 아이템(예: Ivory Tower)은 엔진이 성공해도 0을 반환하므로 회신은 항상 수락으로 처리됩니다 — 인벤토리 그 칸이 비었는지 확인하세요. (내부 메커니즘: W3P use_item (칸 번호 기준))
- `drop_item(hero, slot: 'int', x: 'float', y: 'float')` [verified] [고속 레인] 인벤토리 slot번째 칸의 아이템을 (x,y)에 내려놓습니다(영웅이 걸어가서 내려놓음). (내부 메커니즘: W3P item_drop (JASS UnitDropItemPoint를 그대로 따름: dropitem 0xD0021 지점 대상 + 아이템 즉시 대상))
- `give_item(hero, slot: 'int', to)` [verified] [고속 레인] 인벤토리 slot번째 칸의 아이템을 to (다른 영웅 / 유닛)에게 줍니다(걸어가서 건네줌). 상점에 주면 = 판매(sell_item 참고). (내부 메커니즘: W3P item_drop (JASS UnitDropItemTarget을 그대로 따름: 유닛 대상 dropitem))
- `sell_item(hero, slot: 'int', shop)` [verified] [고속 레인] 인벤토리 slot번째 칸의 아이템을 상점에 팝니다(영웅이 상점 옆까지 가야 함. 판매 가능한 아이템만 받으며, 가격의 절반을 돌려줌). (내부 메커니즘: give_item과 같으며 대상이 상점(실측: Staff of Sanctuary를 125 금에 판매))
- `move_item(hero, slot: 'int', to_slot: 'int')` [verified] [고속 레인] 인벤토리 안에서 칸을 옮깁니다(slot번째 칸을 to_slot번째 칸으로. 두 칸 모두 아이템이 있으면 서로 바꿈). 단축키 배치를 정리할 때 사용합니다. (내부 메커니즘: W3P target: 오더 0xD0022+칸 번호, 대상 = 아이템(JASS UnitDropItemSlot을 그대로 따름))
- `buy(shop, item_code: 'str')` [inferred] [고속 레인] 상점에서 아이템을 삽니다(상점 옆에 서 있는 영웅에게). 기술 선행 조건이 부족하면 엔진이 0을 반환하고 돈을 차감하지 않습니다. (내부 메커니즘: W3P buy: 상점이 옆에 있는 영웅에게 판매)
- `call_to_arms(hall, on: 'bool' = True)` [verified] [고속 레인] 휴먼 Call to Arms: 농부가 민병대로 변합니다(1티어 마을 회관에는 이 능력이 없고, 성채/성에서만 유효). (내부 메커니즘: W3P immediate: townbellon/off)
## 게임 제어
배속, 일시 정지, 발행 주기, 머리 위 말풍선, 캔버스, UI와 입력, 메시지.
- `ui()` [verified] [직접 쓰기] UI와 입력(openwar3.ui.UI): 클릭할 수 있는 버튼과 선택 카드, 단축키, 지면을 클릭해 위치 고르기, 마우스가 가리키는 곳.
버튼 위를 누른 클릭은 게임에 전달되지 않습니다. 순수한 로컬 입력 + 로컬 드로잉이므로 멀티플레이 게임에서도 안전합니다. (내부 메커니즘: W3P 74 input_enable + 공유 메모리 Local\War3Input_(런타임이 창 입력을 받음))
- `set_speed(percent: 'int') -> 'bool'` [verified] [제어 채널] 게임 배속(100 = 기본 속도). (내부 메커니즘: 액션 47(25~800%))
- `pause(on: 'bool' = True)` [verified] [고속 레인] 게임 일시정지 / 재개. 일시정지 중에는 엔진 시계가 멈추지만, 고속 레인으로는 평소처럼 명령을 내릴 수 있습니다(이벤트 디스패치는 계속 돌아감). (내부 메커니즘: W3P pause)
- `set_publish_period(ms: 'int') -> 'None'` [verified] [푸시 스냅샷] 월드 상태의 발행 주기(16~1000밀리초, 기본값 50). 한 번 수집에 약 0.5 ms가 들므로 33 ms도 문제없습니다. 기기 전체가 하나의 값을 공유하며, 마지막에 쓴 값이 적용됩니다. (내부 메커니즘: 월드 블록 requestedPeriodMs)
- `say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'` [verified] [제어 채널] 유닛 머리 위에 채팅 말풍선을 띄웁니다(방송/디버깅용, 게임에 영향 없음). 말풍선이 뜨지 않으면 False를 반환하고, 이유는 g.last_say_error에 있습니다. (내부 메커니즘: 액션 56)
- `message(text: 'str') -> 'bool'` [inferred] [제어 채널] 게임 왼쪽 아래 메시지 영역에 한 줄을 출력합니다(이 PC에서만 보임). 게임이 먼저 알림을 한 번 띄운 뒤에야 동작합니다(DLL이 그때 메시지 창을 잡음). (내부 메커니즘: 액션 45)
- `end_game() -> 'bool'` [verified] [제어 채널] 이 게임 프로세스를 종료합니다(farm.py --keep은 next_game.json에 따라 다음 게임을 자동으로 시작). (내부 메커니즘: 액션 22)
- `canvas()` [verified] [직접 쓰기] 캔버스: 게임 화면 위에 텍스트 상자, 패널, 진행 바, 이미지, 지면의 원과 경로를 그립니다(openwar3.canvas.Canvas).
런타임이 직접 그리므로 게임 핸들을 만들지 않고 게임 상태도 바꾸지 않습니다 — 멀티플레이 게임에서도 안전합니다. 스타일은 자유롭습니다(CJK 텍스트, 둥근 모서리, 반투명). (내부 메커니즘: W3P 73 canvas_enable + 공유 메모리 Local\War3Canvas_(런타임이 매 프레임 게임이 포인터를 그리기 직전에 그림. 포인터가 그 위를 덮음))
- `press_to_continue() -> 'bool'` [verified] [직접 쓰기] “계속하려면 아무 키나 누르세요” 로딩 화면에서 스페이스를 한 번 누릅니다. 많은 RPG / 스토리 맵은 로딩이 끝난 뒤 키를 눌러야 시작합니다(09-24 WarChasers 실측:
누르지 않으면 로딩 화면에 계속 머물고, 게임 시계는 0, 고속 레인도 비워지지 않음). openwar3.run 등은 게임에 들어갈 때 알아서 누르므로 보통 직접 호출할 필요가 없습니다. (내부 메커니즘: PostMessage WM_KEYDOWN/UP 스페이스를 게임 창에 전송(포커스를 빼앗지 않음))
## 샌드박스
JASS 채널: 유닛 생성, 동맹 설정, 이름 변경, 텍스트 표시…… RPG 보조 도구와 동료용입니다. 월드를 바꾸는 것은 싱글플레이 게임과 로컬 도구에서만 가능합니다.
- `jass()` [verified] [고속 레인] 임의의 JASS native를 이름으로 호출합니다: g.jass.CreateUnit(g.jass.Player(1), "Hpal", x, y, 270.0).
인수 I/R/B/S/H는 자동으로 변환되며(유닛/아이템 객체는 그대로 전달), 멀티플레이 게임에서는 읽기 전용 native만 호출할 수 있습니다. 자세한 내용은 openwar3/jass.py와 docs/COMPANION_ZH.md를 참고하세요. (내부 메커니즘: W3P 70 jass(런타임이 이름으로 native 테이블을 조회, 1291개))
- `player_slots() -> 'list[dict]'` [verified] [고속 레인] 플레이어 슬롯 16개: controller(user 사람 / computer / neutral…), state(empty / playing / left), human, me, ally(나와 동맹인지).
RPG 맵에서 동료를 둘 빈 슬롯을 찾거나, 싱글플레이 게임인지 판단할 때 씁니다. (내부 메커니즘: JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly)
- `spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)` [verified] [고속 레인] (x,y)에 유닛 하나를 생성하고(player 기본값은 로컬 플레이어) 스냅샷 속 유닛을 반환합니다(다음 월드 발행까지 대기, 약 50 ms). 생성하지 못하면 None을 반환합니다.
반환된 유닛에는 jass_handle 속성이 하나 더 있습니다. ⚠ 싱글플레이 게임에서만 사용할 수 있습니다(멀티플레이에서는 동기화가 어긋남). (내부 메커니즘: JASS CreateUnit + W3P 72 핸들 -> 유닛)
- `set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'` [verified] [고속 레인] 플레이어 a가 b에 대해 갖는 동맹 관계를 설정합니다: allied = 서로 공격하지 않음 + 서로 지원 요청, vision 시야 공유, control 유닛 제어 공유(b가 a의 유닛을 지휘 가능),
xp 경험치 공유. both=True이면 양방향을 함께 설정합니다(control은 a -> b만). (내부 메커니즘: JASS SetPlayerAlliance)
- `set_player_name(player: 'int', name: 'str') -> 'None'` [verified] [고속 레인] 플레이어 이름을 바꿉니다(점수판, 채팅, 동맹 패널에 표시되는 이름). 동료에게 이름을 붙일 때 씁니다. (내부 메커니즘: JASS SetPlayerName)
- `show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'` [verified] [고속 레인] 화면 왼쪽 아래에 텍스트 한 줄을 표시합니다(맵 트리거가 쓰는 바로 그 텍스트). 기본적으로 로컬 플레이어에게만 보입니다. |cffRRGGBB 색상 코드를 지원합니다. (내부 메커니즘: JASS DisplayTimedTextToPlayer)
## 연결과 도구
연결 상태와 순수 계산 도구.
- `status() -> 'dict'` [verified] [로컬 계산] 연결 상태: pid, 월드 발행(주기, 수집 소요 시간), 고속 레인 카운트. (내부 메커니즘: 월드 블록 + 고속 레인 + 클레임 테이블)
- `nearest(candidates, to)` [verified] [로컬 계산] to (유닛 또는 (x,y))에 가장 가까운 하나. 후보가 없으면 None을 반환합니다. (내부 메커니즘: 순수 계산)