# War3AI / OpenWar3 Documentación completa > Fuente: https://war3ai.com/es. Interfaz abierta de Warcraft III 1.27 para agentes de IA. Al escribir un Bot, usa solo los métodos de Game que aparecen en el "Catálogo de API" al final. --- # Visión general de la documentación > Documentación de OpenWar3: qué es, qué hace; Inicio rápido, Tu primer Bot, Escribe un Bot con un LLM, API y protocolo, Pasarela y MCP — por dónde empezar depende de tu caso. **OpenWar3** es la capa de interfaz abierta de War3AI: un runtime que se inyecta en Warcraft III 1.27, más un SDK de Python. - Cada **50 ms**, el runtime publica en memoria compartida el estado completo de todo el mapa: recursos y comida de todos los jugadores; vida, maná, órdenes, a quién ataca, enfriamientos de habilidades, buffs e inventario de todas las unidades; objetos en el suelo, árboles, colas de producción, día y noche. También hay un **flujo de eventos**: unidades que aparecen y mueren, cada golpe de daño, producción completada… - Los programas externos envían **comandos semánticos** con una latencia de **aproximadamente un fotograma**: mover, atacar, recolectar, construir, entrenar, lanzar hechizos, aprender habilidades, revivir, usar objetos, comprar… Cada comando tiene su **recibo**, que indica si el motor lo aceptó y, si no, el código de motivo. - Tú solo dices "qué hacer": las unidades se nombran por código de cuatro caracteres y las habilidades por nombre de orden, igual que en el juego; "cómo lograrlo" es cosa del runtime. Por eso un LLM no necesita ningún conocimiento de bajo nivel ni mirar la pantalla. Tras leer la documentación puede escribir un Bot que gestiona la economía y sabe combatir y, una vez en partida, corregirlo él mismo a partir de los recibos y los eventos. Y no solo para combatir: el [lienzo](https://war3ai.com/es/docs/canvas/) dibuja tus propios paneles y marcas sobre la pantalla del juego, [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/) hace que los botones dibujados se puedan pulsar y que los atajos de teclado respondan, el [canal JASS](https://war3ai.com/es/docs/jass/) llama desde fuera a las 1291 funciones del juego y, en los mapas RPG, puedes llevar tu propio [compañero IA](https://war3ai.com/es/docs/companion/). La IA que escribas se puede convertir en un [esquema](https://war3ai.com/es/docs/schemes/): se cambia con un clic y se exporta para compartirla; un modo de juego nuevo completo se puede escribir como [mod de juego](https://war3ai.com/es/docs/mods/). También sin escribir Python: la [pasarela](https://war3ai.com/es/docs/gateway/) permite a cualquier lenguaje o página web llamar a las mismas interfaces por WebSocket / JSON, y el [servidor MCP](https://war3ai.com/es/docs/mcp/) permite que agentes como Claude Code llamen directamente a herramientas para ver la partida y dar órdenes. - [Inicio rápido](https://war3ai.com/es/docs/quickstart/): Prepara el entorno, lanza una partida con un solo comando y mira cómo toma el control un Bot de ejemplo. - [Escribe un Bot con un LLM](https://war3ai.com/es/docs/ai-bot/): No hace falta saber programar: copia el prompt, describe tu estrategia y dáselo a un agente. - [Modelo mental](https://war3ai.com/es/docs/concepts/): Snapshots, comandos, recibos, eventos y ticks. Cinco minutos de lectura antes de escribir un Bot. - [Catálogo de API](https://war3ai.com/es/api/): Todas las interfaces, cada una con su estado de pruebas, su nivel de latencia y su mecanismo de bajo nivel. ## Elige un camino según tu caso | Si… | Empieza por | Después | |---|---|---| | Juegas a Warcraft pero no sabes programar | [Inicio rápido](https://war3ai.com/es/docs/quickstart/) → [Escribe un Bot con un LLM](https://war3ai.com/es/docs/ai-bot/) | Si algo falla, consulta las [Preguntas frecuentes](https://war3ai.com/es/docs/faq/) | | Sabes Python | [Tu primer Bot](https://war3ai.com/es/docs/first-bot/) → [Modelo mental](https://war3ai.com/es/docs/concepts/) → [Las quince reglas](https://war3ai.com/es/docs/rules/) | [Recetario de juego pro](https://war3ai.com/es/docs/cookbook/), [Bots de ejemplo](https://war3ai.com/es/docs/examples/) | | Construyes un coding agent / automatización | [Iteración autónoma del agente](https://war3ai.com/es/docs/agent-loop/) | [Recibos y códigos de motivo](https://war3ai.com/es/docs/reason-codes/), [`llms-full.txt`](https://war3ai.com/es/llms-full.txt) | | Quieres que un LLM tome decisiones durante la partida | [El LLM como asesor](https://war3ai.com/es/docs/llm-coach/) | [Burbujas y modelos locales](https://war3ai.com/es/docs/speech/) | | Quieres que un agente actúe directamente (Claude Code, etc.) | [El LLM usa herramientas directamente (MCP)](https://war3ai.com/es/docs/mcp/) | [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/) | | Usas otro lenguaje (JS, C#, Go, Rust…) | [Pasarela](https://war3ai.com/es/docs/gateway/) | Más a bajo nivel: [Protocolo W3P](https://war3ai.com/es/docs/protocol/) | | Quieres enfrentar IA de distintas personas | [Modo justo](https://war3ai.com/es/docs/fair-mode/) | [Arena](https://war3ai.com/es/arena/) | | Quieres crear tu propio modo de juego en mapas RPG / personalizados | [Mods de juego](https://war3ai.com/es/docs/mods/) | [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/), [Lienzo](https://war3ai.com/es/docs/canvas/), [Canal JASS](https://war3ai.com/es/docs/jass/), [Compañero para RPG](https://war3ai.com/es/docs/companion/) | | Quieres compartir tu IA con otros | [Esquemas de IA](https://war3ai.com/es/docs/schemes/) | [Consola Farsight](https://war3ai.com/es/docs/console/) | ## Qué hay en el repositorio ```text start.bat Único punto de entrada: despliegue desde cero + abre Farsight; stop.bat lo detiene todo por completo sdk/python/ Capa de interfaz. openwar3/ es la fachada pública (Game + Bot): empieza por aquí brains/ Capa de decisión examples/ hello_bot (economía) → rush_bot (tropas) → macro_bot (macro) → micro_bot (micro + creeping); buddy (compañero para RPG); mod_hero_roguelike / mod_endless_defense (mods de juego) xwar3/ Cerebro de referencia: capa de estrategia (segundos) + capa de reflejos (4 procesos) + modelo de victoria console/ Consola web Farsight (FastAPI + React) gateway/ Pasarela (WebSocket / JSON) + cliente JS + página de demostración para el navegador director/ Cámara automática, barras de vida sobre las unidades speech/ Burbujas de diálogo + LLM local runtime/ Orquestación multiinstancia (cada partida se reinicia según su configuración) data/ order-ids.txt; herramientas para extraer datos de tu propia copia del juego schemes/ Tus esquemas de IA (mine/) y los que comparten otros (installed/); no entran en el repositorio tools/ play.py (una partida con un comando), run_scheme.py (ejecutor de esquemas), war3_mcp.py (servidor MCP), run_tests.py, scripts de verificación en vivo docs/ Catálogo de API api.json (generado desde el código), protocolo, manual ``` Entre el runtime y tu código solo hay un [protocolo W3P](https://war3ai.com/es/docs/protocol/) versionado: el SDK de Python es lo más cómodo, pero también puedes conectarte desde otro lenguaje siguiendo el protocolo. ## Qué significa el "estado de pruebas" de una interfaz Cada interfaz del catálogo lleva uno de estos tres estados: - **Verificado en partida**: la ruta de bajo nivel (ID de acción, forma de los parámetros, efecto leído) se ha verificado en partidas reales y la protege un script de verificación. - **Experimental**: interfaz nueva que ya funciona en la instancia de prueba y se está verificando punto por punto en partidas reales. Se puede usar, pero los detalles de la interfaz aún pueden cambiar. - **Inferido / no probado del todo**: el mecanismo copia lo que hace el propio motor (por ejemplo, la función JASS equivalente), pero aún no se ha comprobado punto por punto en partida. Revisa el recibo antes de fiarte. > **Nota** > > Por ahora solo se admite **Warcraft III 1.27** (The Frozen Throne). Las versiones 1.24 ~ 1.28 comparten la misma estructura de motor, y el soporte multiversión es la fase P4 de la [hoja de ruta](https://war3ai.com/es/roadmap/). A partir de 1.29, y en Reforged, el motor es otro y queda fuera de lo que prometemos. --- # Inicio rápido > Doble clic en start.bat y todo se instala solo; configura el directorio del juego en Farsight, lanza una partida y mira cómo un Bot de ejemplo toma el control. Unos 15 minutos. ## Lo que necesitas | | Requisito | Notas | |---|---|---| | Sistema | Windows 10 / 11, 64 bits | Por ahora solo se admite Windows | | Juego | Warcraft III **1.27a** (The Frozen Throne, `Game.dll` 1.27.0.52240) | Un cliente que poseas legalmente; no se modifica ningún archivo del juego en disco | **No hace falta instalar nada más antes.** `start.bat` descarga una sola cosa: Python 3.13 (paquete portátil oficial, unos 14 MB), que coloca en `bin\env\` dentro del repositorio; no pide permisos de administrador, no cambia el PATH del sistema y, desde redes de China continental, cambia automáticamente a servidores espejo. Si tu equipo ya tiene una versión de Python que sirve, la usa directamente. El PowerShell es el que trae Windows; la web de Farsight se entrega ya compilada con el repositorio, así que no hace falta Node.js. ## Instalación 1. **Consigue el código** ```bash git clone https://github.com/OPENXXAI/OpenWar3AI.git ``` O descarga el [archivo comprimido](https://github.com/OPENXXAI/OpenWar3AI/archive/refs/heads/main.zip) y descomprímelo. El runtime (la DLL de inyección y el lanzador) viene incluido en el repositorio; no hay que descargarlo aparte. 2. **Haz doble clic en `start.bat`** La primera vez, él solo: - descarga Python 3.13; - instala los paquetes de Python y, tras comprobar los archivos del runtime, los deja instalados; - descarga AMAI y genera los datos de estrategia que usa el cerebro de referencia (AMAI tiene una licencia propia y lo generado no entra en git; si falla, solo afecta al cerebro de referencia); - abre la página de inicio de Farsight, el "Centro de control": `http://127.0.0.1:8866`. Cada paso imprime su resultado y, si alguno falla, te dice cómo completarlo. Las veces siguientes, cada doble clic solo hace una comprobación de uno o dos segundos y abre Farsight. La ventana negra se cierra sola a los pocos segundos: Farsight sigue ejecutándose en segundo plano y no se detiene aunque cierres el navegador. 3. **Configura el directorio del juego en el Centro de control** En la parte superior del Centro de control puedes usar "Buscar automáticamente" o elegir tú el directorio de Warcraft III con "Examinar…". Farsight comprueba la versión del juego y **extrae los datos de tu propio juego** (tabla de unidades, habilidades, objetos, tabla de counters…; los archivos de Blizzard no se distribuyen con el código). Si la versión no es la 1.27a, te avisa. Los mapas y la configuración de la siguiente partida son relativos a este directorio (se pueden elegir los mapas de todas las carpetas de `\Maps`); para cambiar de directorio más adelante, ve a la página "Ajustes". 4. **Lanza una partida y deja que un Bot de ejemplo tome el control** Lo más cómodo es ir a la página "Instancias y partida" de Farsight, marcar un número de instancia, elegir un esquema de IA y pulsar "Iniciar prueba". También puedes usar la línea de comandos: ```bash python tools/play.py --bot brains/examples/hello_bot.py ``` Este comando arranca una instancia del juego, inyecta el runtime, inicia la partida automáticamente y luego lanza el Bot. **Si ves a los campesinos ir a la mina y al ayuntamiento entrenar campesinos, ya está.** El `python` del comando es el que figura en `openwar3.json`; el que instala `start.bat` por su cuenta está en `bin\env\python\python.exe`. ## start.bat y stop.bat ```bash start.bat # comprobación del despliegue + abre Farsight start.bat setup # comprobación completa: reinstala los paquetes de Python, reintenta AMAI start.bat restart # solo reinicia el backend de Farsight (el juego y los servicios no se ven afectados) start.bat node # instala además una copia de Node.js (solo hace falta para previsualizar la web; normalmente no se usa) start.bat 5 6 # además inicia la prueba en las instancias 5 y 6 (juego + cerebro de referencia) stop.bat # lo detiene todo por completo; stop.bat --keep-llm deja el modelo local en la VRAM ``` La pasarela, las burbujas de diálogo y el LLM local también se inician y se detienen desde el "Centro de control" de Farsight, sin tener que buscar otros scripts. **Para detenerlo todo por completo**: haz doble clic en `stop.bat` o pulsa "Detener todo" en la esquina superior derecha del Centro de control; se detienen por orden las instancias del juego, la IA, la pasarela, las burbujas, el modelo local que usa este sistema y el backend de Farsight. El servidor MCP lo gestionan clientes como Claude, así que no se detiene. > **Archivo de configuración** > > `openwar3.json` lo escriben automáticamente `start.bat` y Farsight; solo guarda rutas locales y no entra en git. Si quieres cambiar los puertos o la dirección y el nombre del modelo del LLM local, toma como referencia `openwar3.example.json` y escribe solo las opciones que difieran de él. ## Parámetros de 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 # el juego ya está abierto; solo conecta el Bot python tools/play.py --bot my_bot.py --fair # modo justo: solo ve lo que está en su campo de visión ``` | Parámetro | Por defecto | Descripción | |---|---|---| | `--bot` | Obligatorio | Ruta del archivo del Bot (debe contener una subclase de `Bot`) | | `--inst` | `9` | Número de instancia. No uses el de una instancia que ya esté en marcha (la página "Instancias y partida" de Farsight muestra qué números están en uso) | | `--race` | `1` | Nuestra raza: 1 Humanos, 2 Orcos, 3 No-muertos, 4 Elfos de la noche | | `--enemy-race` | `0` | Raza del rival | | `--difficulty` | `2` | Dificultad del ordenador: 2 fácil, 3 normal, 4 demente | | `--speed` | `100` | Velocidad de juego (porcentaje; 200 = 2×) | | `--map` | `default_map` de la configuración | Mapa | | `--attach` | | No arranca el juego; solo se conecta a una instancia ya en marcha | | `--hz` | `5` | Cuántas veces por segundo se llama a `on_tick` | | `--minutes` | `60` | Duración máxima en minutos (reloj real) | | `--fair` | | [Modo justo](https://war3ai.com/es/docs/fair-mode/) | | `--player` | | Número de jugador que controla el Bot (para IA contra IA) | > **Atención** > > No arranques el juego con `--minimize`: **con la ventana minimizada, la simulación del juego se detiene** (el reloj no avanza) y el Bot se quedará esperando para siempre a que empiece la partida. También puedes prescindir de `play.py` y conectarte a una instancia ya en marcha con la línea de comandos del SDK: ```bash python -m openwar3 run brains/examples/hello_bot.py --inst 5 # ejecutar el Bot python -m openwar3 status --inst 5 # conectar e imprimir el estado de la instantánea / del carril rápido python -m openwar3 catalog # imprimir el catálogo de la API ``` ## Cuando ya funcione - [Escribe tu primer Bot](https://war3ai.com/es/docs/first-bot/): Empieza con un Bot mínimo de 10 líneas y añade paso a paso tropas y ataques. - [Deja que un LLM lo escriba por ti](https://war3ai.com/es/docs/ai-bot/): Copia la plantilla de prompt y describe tu estrategia con palabras sencillas. ## Autocomprobación ```bash python tools/run_tests.py # SDK / cerebro de referencia / capa de reflejos / consola / bocadillos / ejemplos, cada suite en un subproceso ``` Los tests offline no necesitan abrir el juego. En el "Centro de control" de Farsight también hay una comprobación del entorno que muestra si cada parte está instalada. --- # Tu primer Bot > Empieza con un Bot mínimo de 10 líneas, añade campesinos, comida, tropas, un héroe y un ataque, y aprende a leer los recibos. Un Bot es una clase que hereda de `openwar3.Bot`. Solo tienes que sobrescribir los hooks que necesites; `g` (`Game`) se encarga de «observar» y «actuar». ## El Bot mínimo ```python title="my_bot.py" from openwar3 import Bot class MyBot(Bot): def on_start(self, g): # se llama una vez al entrar en la partida g.message("¡Ya estoy aquí!") def on_tick(self, g): # unas 5 veces por segundo for w in g.idle_workers(): g.gather(w, g.nearest(g.gold_mines(), w)) ``` ```bash python tools/play.py --bot my_bot.py ``` Los campesinos ociosos irán a la mina de oro más cercana. Estos son los cuatro hooks: | Hook | Cuándo se llama | |---|---| | `on_start(g)` | Una vez, al entrar en la partida y antes del primer tick | | `on_tick(g)` | En cada tick (por defecto, 5 veces por segundo). Si un tick se pasa de tiempo, el siguiente se aplaza automáticamente; no se acumulan | | `on_event(g, ev)` | Antes de cada `on_tick`; te entrega uno a uno los eventos ocurridos desde el tick anterior | | `on_end(g, reason)` | Una vez, cuando termina la partida (el proceso del juego desaparece / no nos quedan unidades / parada manual) | > **Consejo** > > Una excepción dentro de `on_tick` no interrumpe la partida: el ejecutor imprime el stack trace y sigue en el siguiente tick; solo se detiene tras **20 ticks seguidos con error**. ## Añade la economía: campesinos y comida ```python from openwar3 import Bot class Economy(Bot): def on_tick(self, g): res = g.resources() # si no se puede leer es None, no 0 halls = g.my_buildings({"htow", "hkee", "hcas"}) if res is None or not halls: return home = halls[0] # 1. Los campesinos ociosos van a por oro for w in g.idle_workers(): mine = g.nearest(g.gold_mines(), w) if mine: g.gather(w, mine) # 2. Entrenar campesinos: solo 1 en la cola (llenarla deja el dinero bloqueado en la cola) if len(g.my_workers()) < 15 and not g.queue(home): g.train(home, "hpea") # 3. Comida casi al límite: busca un campesino que no esté construyendo y levanta una granja cerca del ayuntamiento 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) ``` Tres detalles que vale la pena destacar: - **Entrena solo cuando `g.queue(home)` está vacía.** Dar la orden de entrenar en cada tick llena la cola de 7 casillas y bloquea el dinero (medido: el ayuntamiento acabó con 4 campesinos en cola y 300 de oro bloqueados, y la apertura se retrasó muchísimo). - **`build_near` en lugar de coordenadas fijas.** Busca por sí solo, de cerca a lejos, un sitio donde quepa el edificio y sigue el resultado entre ticks; si falta dinero, no hace nada. Unas coordenadas fijas pueden caer justo en un bosque. - **No elijas a un campesino que ya esté construyendo.** Una granja humana tarda 35 segundos en construirse; si te llevas al trabajador a mitad de obra, los cimientos se quedan parados. La versión completa, que funciona con las cuatro razas, es `brains/examples/hello_bot.py`: 5 por mina, a talar cuando la mina está llena y retomar los cimientos abandonados. ## Añade cuartel, héroe y ataque ```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] # Héroe: si hay altar y no hay héroe -> primero intenta revivir; si no se puede, entrena (el héroe es único; entrenar otro tras su muerte se rechaza) 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") # Luz sagrada # El cuartel saca soldados sin parar (solo 1 en la cola) for b in g.my_buildings({"hbar"}): if not g.queue(b): g.train(b, "hfoo") # Con una oleada completa, al ataque; si queda destrozada, a casa 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)] # órdenes solo a las ociosas g.attack_move(idle, target.x, target.y) ``` La versión completa está en `brains/examples/rush_bot.py` (hereda de `hello_bot` y construye el cuartel / altar si no existen). ## Aprende a leer los recibos Cada comando devuelve un recibo. `if r:` significa «el motor lo aceptó»; si no lo aceptó, `r.reason` dice por qué: ```python r = g.train(barracks, "hfoo") if not r: print(r.reason) # rejected(人口不够) = "falta comida" print(r.verdict) # 3 ``` Códigos de motivo frecuentes: `3` falta comida, `8` falta oro, `9` falta madera, `32` cola llena, `183` falta un requisito previo, `221` no existe esa opción / en construcción / ese héroe ya existe, `1001` el objetivo no es visible. La tabla completa está en [Recibos y códigos de motivo](https://war3ai.com/es/docs/reason-codes/). > **Aceptado ≠ conseguido** > > El recibo solo indica que «el motor aceptó el comando». El motor también acepta en el acto un punto de construcción dentro de un bosque, y el trabajador solo falla al llegar; un hechizo puede ser interrumpido. Para ver el efecto, mira la instantánea y los eventos: para construir usa `build_near` (sigue si aparecen los cimientos) y, para los hechizos, comprueba si `g.cooldown()` entró en enfriamiento. ## Siguientes pasos - [Modelo mental](https://war3ai.com/es/docs/concepts/): Instantánea, comando, evento, tick y lote: por qué está diseñado así. - [Recetario de jugadas profesionales](https://war3ai.com/es/docs/cookbook/): 21 recetas: minas saturadas, comida sin atascos, fuego concentrado, retirar heridos, creepear de noche… --- # Escribe un Bot con un LLM > Puedes hacerlo sin saber programar: tú explicas bien cómo quieres que juegue y el LLM escribe el código. Copia la plantilla de prompt, describe tu estrategia, ejecútalo y pídele cambios. Pensado para quien sabe jugar a Warcraft pero no sabe programar, y también para desarrolladores que quieren ahorrar tiempo. Todo el proceso es una conversación: **tú describes la estrategia → el modelo escribe el código → juegas una partida → le cuentas al modelo lo que pasó → lo corrige**. > **Consejo** > > Primero prepara el entorno siguiendo el [Inicio rápido](https://war3ai.com/es/docs/quickstart/) y haz funcionar `hello_bot` (verás a los campesinos ir a la mina). Así, cuando algo falle, sabrás distinguir si es un problema del entorno o del Bot. ## 1. Prepara el material para el modelo Lo bien que escriba el modelo depende en un ochenta por ciento de que haya leído el material correcto. Elige una opción según la herramienta que uses: | Lo que usas | Cómo darle el material | |---|---| | **Un Coding Agent que lee el repositorio** (Claude Code, Cursor, Codex, etc.) | Ábrelo en el directorio del repositorio y pídele que lea primero `docs/BOT_HANDBOOK_ZH.md`, `docs/api.json` y un ejemplo (para economía, `brains/examples/macro_bot.py`; para combate, `micro_bot.py`) | | **Un modelo de chat con acceso a internet** | Pídele que lea primero [`https://war3ai.com/llms-full.txt`](https://war3ai.com/es/llms-full.txt): toda la documentación del sitio está en ese único archivo | | **Chat web sin acceso a internet** | Pega el manual, [`api.json`](https://war3ai.com/es/api.json) y un archivo de ejemplo después del prompt | | **Un modelo local** (LM Studio, Ollama) | Igual que el anterior. Se recomienda una ventana de contexto de más de 32K tokens; si no, el manual y el catálogo de la API no caben | Si quieres una jugada profesional concreta, pega además la receta correspondiente del [Recetario de jugadas profesionales](https://war3ai.com/es/docs/cookbook/). ## 2. Copia este prompt Sustituye «La estrategia que quiero» del final por tus propias palabras; cuanto más concreto, mejor: ```text Vas a escribir una IA (en Python) para Warcraft III 1.27. Usa solo los métodos de Game que aparecen en api.json; no inventes métodos que no existen. Sigue el estilo de rush_bot.py: hereda de openwar3.Bot e implementa on_start(g) y on_tick(g). Reglas: - on_tick se llama unas 5 veces por segundo; tiene que ser rápido (no uses sleep dentro). - Un valor que no se puede leer es None, no 0: compruébalo antes de usarlo. - Cada comando devuelve un recibo (Receipt); `if r:` significa "el motor lo aceptó"; si no lo aceptó, `r.reason` indica el motivo (falta comida, falta oro, el objetivo no es visible, ese héroe ya existe…); vuelve a intentarlo en el siguiente tick o prueba otra cosa. - Para atacar a un enemigo concreto usa g.attack(unidades, enemigo); el enemigo tiene que estar a la vista: si no se ve, se rechaza. - Si un héroe muere, revívelo con g.revive(altar); no se puede entrenar otro. - Para construir usa g.build_near(trabajador, código_edificio, x, y): busca solo un sitio donde quepa, sigue el resultado y no hace nada si falta dinero. - Para saber "qué acaba de pasar" (quién murió, quién perdió vida, un héroe subió de nivel, cayó un objeto) implementa on_event(g, ev). - No repitas la misma orden a la misma unidad en cada tick (interrumpe lo que está haciendo); da órdenes a las unidades "ociosas". - Manda a recolectar solo a los trabajadores de idle_workers(). Como máximo 5 trabajadores por mina de oro. - Pon solo 1 unidad en la cola de entrenamiento (encola otra cuando g.queue(edificio) esté vacía); si la comida se atasca, mira g.production(edificio).blocked. - Si en un tick tienes que dar muchos comandos, envuélvelos en with g.batch(): (se espera una sola vez al hilo del juego). - Para elegir a quién atacar usa g.time_to_kill(mi_grupo, enemigo) (tiene en cuenta counters y armadura); para elegir adónde ir usa g.path_distance (devuelve None si no se puede llegar). - En modo justo solo se ve lo que está en tu campo de visión; para los enemigos vistos antes usa g.last_seen(). - Las unidades se identifican con códigos de cuatro caracteres (campesino humano hpea, soldado hfoo, cuartel hbar…); los hechizos, con nombres de orden (thunderbolt Martillo de tormenta, blizzard Ventisca, holybolt Luz sagrada…; la lista completa está en data/order-ids.txt); para aprender habilidades se usan códigos de cuatro caracteres (AHtb, AHbz…). La estrategia que quiero: ``` ### Cómo explicar bien la estrategia Lo que peor lleva el modelo son los requisitos vagos. En lugar de «juega más agresivo», esta información es mucho más útil: - **Raza y héroes**: qué héroe sale primero y en qué orden sube habilidades (por ejemplo, Archimago: Elemental de agua, Ventisca, Elemental de agua…). - **Orden de construcción**: con cuántos campesinos se levanta el cuartel, cuándo subir de tier, cuántos cuarteles. - **Composición del ejército**: ¿soldados + fusileros? ¿Con cuántos sale a atacar? - **Condiciones para avanzar y retirarse**: con cuántas unidades ataca, con cuánta vida se retira el héroe, volver a casa a reagruparse si el ejército queda destrozado. - **Creeping**: si creepea o no, cuándo (¿al anochecer?), ¿solo los campamentos que puede ganar? - **Justo o no**: si en el futuro quieres competir en la Arena, dilo: «usa solo los enemigos que se ven en el campo de visión». ## 3. Ejecútalo Guarda el código que te dé el modelo como `brains/my_bot.py` y luego: ```bash python tools/play.py --bot brains/my_bot.py --race 1 --difficulty 2 ``` Para ver el resultado antes, añade `--speed 200` (velocidad 2×). ## 4. Pídele cambios - **Da un error**: pega el **error completo**, tal cual, y dile al modelo «corrígelo». - **Juega mal**: describe **lo que ves en el juego**, no la causa que supones. Por ejemplo: «el héroe se queda quieto en casa», «las unidades llegan de una en una y mueren», «los campesinos se amontonan en una sola mina». - **Quieres añadir una estrategia nueva**: añade una sola cosa cada vez, juega una partida para confirmar que nada se ha roto y pasa a la siguiente. > **Nota** > > Un Coding Agent que puede ejecutar comandos puede encargarse también de los pasos 3 y 4: jugar una partida, leer los logs y los recibos, cambiar el código y volver a jugar. Cómo darle suficiente información: consulta [Iteración autónoma del Agent](https://war3ai.com/es/docs/agent-loop/). ## 5. Problemas frecuentes | Síntoma | Casi siempre es | |---|---| | No se mueve nada | Número de instancia incorrecto (`--inst`), o el juego todavía no ha entrado en la partida | | Los campesinos no minan | Das órdenes a campesinos que ya están trabajando; manda solo a los de `idle_workers()` | | Nunca termina de construir casas | Usa `build_near` en vez de coordenadas fijas; mira si el `reason` del recibo es falta de dinero | | No sale el héroe | Mira el recibo de `train`: ¿falta comida? ¿O el héroe murió (hace falta `revive`)? | | El héroe no lanza hechizos | No los ha aprendido (`learn`) o no tiene maná; tras lanzar, mira si `cooldown()` entró en enfriamiento | | Las unidades tiemblan tick a tick | Se les vuelve a dar la orden en cada tick; da órdenes solo a las unidades ociosas | | No salen unidades y el dinero no para de subir | La comida está atascada: mira `g.production(cuartel).blocked` | | El modelo usa métodos que no existen | Insiste en el prompt en «usa solo los métodos de api.json» y pega api.json completo | ## Siguientes pasos - Todas las interfaces y el mecanismo subyacente de cada una: [catálogo de la API](https://war3ai.com/es/api/); - El cerebro de referencia (`brains/xwar3/strategy`) es una IA completa que se expande, creepea y ataca. Puedes pedirle al modelo que lea sus ideas, pero usa interfaces de más bajo nivel, así que no se recomienda copiarlo tal cual; - Cuando compitas en la [Arena](https://war3ai.com/es/arena/), solo verás a los enemigos que estén en tu campo de visión: añade ya `--fair` para imponerte esa restricción y no tener que cambiar nada después. --- # Iteración autónoma del Agent > Deja que un Coding Agent juegue partidas, lea los resultados, cambie el código y vuelva a jugar por su cuenta. Necesita un comando que funcione sin supervisión, un informe de partida estructurado y un objetivo claro. En [Escribe un Bot con un LLM](https://war3ai.com/es/docs/ai-bot/), el paso «jugar una partida → ver qué pasa → contárselo al modelo» lo haces tú. Un Coding Agent capaz de ejecutar comandos (Claude Code, Codex, el modo Agent de Cursor, etc.) puede encargarse también de ese paso y cerrar el ciclo: ```text cambiar código ──► jugar una partida (sin supervisión) ──► leer el informe ──► hallar lo que más pesa ──┐ ▲ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ ``` Para que ese ciclo converja de verdad, el Agent necesita tres cosas. ## 1. Un comando que funcione sin supervisión ```bash python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair ``` - `--minutes` garantiza que la partida termine (minutos de reloj real), así el Agent no se queda atascado en una partida; - `--speed 200` usa velocidad 2× para ahorrar tiempo, pero dentro del Bot **espera según el reloj del juego** (`g.clock()`), no con `sleep` según el reloj real; - `--fair` hace que escriba desde el primer día con las reglas de la Arena: solo ve lo que está en su campo de visión; - Al terminar, la terminal imprime el motivo del final, por ejemplo `我方没有单位了` (no nos quedan unidades) o `到时间了` (se acabó el tiempo); lo que el Bot imprima con `print` también aparece en la terminal. > **Atención** > > Con la ventana minimizada, la simulación del juego se detiene. Haz que el Agent arranque el juego en el modo de ventana por defecto y que no use el mismo número de instancia que la que estás usando tú (`--inst`). ## 2. Un informe de partida estructurado La salida de la terminal es para personas. Lo que debe leer el Agent es un JSON: qué pasó, qué no se consiguió y por qué. El SDK ya te da toda la materia prima: los recibos traen códigos de motivo y el flujo de eventos trae producciones terminadas y bajas. Basta con recopilarlos: ```python title="recorder.py" import collections, json, time from openwar3 import Bot class Recorder(Bot): """Añade un informe de partida a un Bot. Hereda de esta clase y llama a super() desde tus propios on_start / on_event.""" def on_start(self, g): self.rejects = collections.Counter() # "train hfoo: rejected(人口不够)" -> número de veces self.timeline = [] # [segundo de juego, categoría, código de cuatro caracteres]: entrenamiento / investigación / construcción / mejora terminados self.lost = collections.Counter() # qué hemos perdido self.killed = collections.Counter() # qué hemos matado def check(self, r, what): """Envuelve un comando para registrar el motivo del rechazo: 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: # puede que el juego ya se haya cerrado; si no se puede leer, da igual 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) ``` Preguntas que puede responder este informe: | Señal | De dónde sale | Qué revela | |---|---|---| | Motivos de rechazo más frecuentes | `reason` / `verdict` del recibo | La comida siempre atascada (3), órdenes una y otra vez sin dinero suficiente (8 / 9), ataques a objetivos en la niebla (1001), entrenar un héroe que ya murió (221) | | Línea de tiempo de producción | Eventos `production.done` (con los segundos de juego que tardó) | En qué segundo sale el primer héroe, en qué segundo sube de tier, si el cuartel produce sin parar; se puede comparar con la apertura de un jugador profesional | | Bajas de ambos bandos | Eventos `unit.died` | Si está regalando unidades todo el rato, cuántas veces murió el héroe, si el creeping salió a cuenta | | Motivo del final | `on_end(g, reason)` | `我方没有单位了` = derrota; `到时间了` = todavía sin ganador | | Ejército y recursos finales | Una lectura de la instantánea en `on_end` | Dinero acumulado sin gastar = la producción no da abasto; pocos trabajadores = la economía no despegó | > **Nota** > > Determinar la victoria por programa es uno de los experimentos de base de la [Arena](https://war3ai.com/es/arena/) y todavía está en la hoja de ruta. Por ahora puedes considerar derrota `我方没有单位了` (no nos quedan unidades) y aproximar la victoria con «todos los edificios enemigos visibles destruidos». ## 3. Un objetivo claro y algunas restricciones Pásale esto al Agent, adaptado a tu objetivo: ```text Objetivo: que brains/my_bot.py gane de forma estable en Echo Isles al ordenador en dificultad "Fácil" (Humanos contra raza aleatoria). En cada ronda: 1. Ejecuta python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair 2. Lee la salida de la terminal y el run_*.json más reciente: motivo del final, línea de tiempo de producción, motivos de rechazo más frecuentes, bajas de ambos bandos 3. Encuentra "un solo" problema, el que más afecte al resultado, y cambia solo eso; escribe en un comentario del código el motivo del cambio y los datos en que te basas 4. Vuelve al paso 1. Si no hay mejora en 3 partidas seguidas, detente y cuéntame el informe y tu valoración Restricciones: - Usa solo los métodos de docs/api.json; no inventes interfaces - No repitas la misma orden a la misma unidad en cada tick; da órdenes solo a las unidades ociosas - Mantén --fair (usa solo los enemigos que se ven en el campo de visión) - Antes de cambiar el código, ejecuta python tools/run_tests.py para confirmar que no has roto los ejemplos ``` ## Hábitos para que el ciclo converja más rápido - **Cambia una sola cosa cada vez.** Si cambias tres a la vez y ganas, no sabes cuál funcionó; si pierdes, tampoco sabes cuál lo rompió. - **Juega suficientes partidas para comparar.** La misma situación tiene mucha aleatoriedad; con dos partidas solo se detectan diferencias muy grandes. Para decidir si «hay mejora», mira la tendencia de varias partidas. - **Arregla primero los «rechazos» y luego ajusta la estrategia.** El motivo de rechazo más frecuente en los recibos suele ser el mayor bug del Bot. - **Escribe tu razonamiento en los comentarios.** El Agent de la siguiente ronda (o la siguiente conversación) sabrá por los comentarios por qué el código es así y no deshará lo que ya estaba arreglado. - **Red de seguridad con tests offline.** Escribe tests unitarios que no necesiten abrir el juego para la lógica clave (los tests de los Bots de ejemplo están en `brains/examples/tests/`), y haz que el Agent los ejecute después de cada cambio. --- # El LLM como asesor > Deja en manos del LLM qué acumular, dónde poner a los trabajadores y si este minuto toca atacar o aguantar; la capa de reglas solo ejecuta y veta. El cerebro de referencia ya funciona así; esta página explica el patrón y sus trampas. Cuando tu Bot alcanza cierto tamaño, descubres que las reglas de la capa económica se han ido pegando una encima de otra: una para el número de leñadores, otra para 5 por mina, otra para reducirlos a la mitad cuando sobra madera, otra para mandar más cuando hay mucho oro y poca madera… Cada una por separado es correcta, pero juntas producen situaciones como «a la mina le faltan trabajadores y todos los campesinos están talando», de las que **ninguna regla se hace responsable**. Este tipo de juicio —ver el conjunto y fijar prioridades— no encaja bien en un `if / else`, pero es justo lo que mejor hacen los LLM. El cerebro de referencia (`brains/xwar3/strategy/brain/coach.py`) usa exactamente la división en capas que ves abajo. ## Capas ```text LLM (asesor) una vez cada 20 segundos de juego, asíncrono, nunca bloquea un tick Entrada: una página con el estado de la partida (recursos, comida, reparto de campesinos, minas, unidades, tecnologías, héroes, información del enemigo, sucesos recientes) Salida: JSON estricto —— un diagnóstico de una frase + reparto de trabajadores + qué producir primero + postura para este minuto + qué no hacer │ ▼ lista blanca + acotación a mínimos / máximos + veto Capa de reglas (Bot, cada tick) traduce la sugerencia en «sesgos» sobre capacidades que ya existen: reparto de trabajadores, prioridades de construcción / entrenamiento, postura de ataque │ ▼ Capa de ejecución (SDK / capa de reflejos) da órdenes, lee recibos, hace el micro ``` ## Contrato de salida Haz que el modelo emita solo JSON con campos fijos, sin añadir ni quitar ninguno: ```json { "diagnosis": "Una frase: el mayor problema de la partida; tiene que poder justificarse con los datos de entrada", "workers": { "gold": 10, "lumber": 6 }, "priority": ["hpea", "hhou", "hbar"], "posture": "creep", "avoid": ["Si falta madera, no investigues Iron Plating primero"] } ``` | Campo | Cómo lo usa la capa de reglas | Acotación en el cerebro de referencia | |---|---|---| | `workers` | Número objetivo de trabajadores en oro y en madera | Oro 2 ~ 25, madera 1 ~ 20; la suma no puede superar el total de campesinos | | `priority` | Orden de prioridad para entrenar / construir / investigar | Como máximo 4; solo se aceptan códigos de cuatro caracteres que aparezcan en la tabla de «códigos permitidos» | | `posture` | La postura para este minuto | Solo uno de `attack` `defend` `creep` `expand` `recover` `hold` | | `avoid` | Qué no hacer este minuto | Como máximo 2 | | `diagnosis` | Solo para los logs y para mostrarlo en la consola | — | Usa un prompt por raza que contenga solo las decisiones propias de esa raza (la construcción asistida y la milicia de los Humanos, la madriguera de los Orcos, la mina de oro encantada de los No-muertos, la mina de oro enredada de los Elfos de la noche…). Las reglas comunes van en una parte compartida: no las copies cuatro veces. ## Cuatro restricciones estrictas Las cuatro le costaron caro al cerebro de referencia: 1. **El asesor nunca da órdenes directas a las unidades.** No ve lo que pasa en la escena a escala de 150 ms y además alucina. Solo cambia objetivos y prioridades; quién va adónde y a quién ataca lo siguen decidiendo la capa de reglas y la capa de reflejos. El mando solo puede tener un dueño. 2. **Asíncrono.** Cada consulta tarda alrededor de 1 segundo, corre en un hilo en segundo plano y se aplica el resultado más reciente: **nunca bloquea un tick**. Si el modelo no ha arrancado, se agota el tiempo o responde cualquier cosa, se actúa como si esta capa no existiera y el comportamiento vuelve a ser solo de reglas. Las sugerencias demasiado viejas (más de 3 intervalos) tampoco se usan. 3. **Lista blanca + acotación.** Cada campo debe poder mapearse a una capacidad existente, y los valores se acotan a rangos razonables. Lo que no se reconoce **se cuenta y se descarta**, en lugar de ignorarse en silencio. 4. **Cuéntalo todo.** Cuántas consultas, cuántos éxitos, cuántos timeouts, cuántas veces se acotó un valor, cuántas veces se adoptó cada campo; publícalo junto con la última entrada enviada al modelo. Si no, la pregunta «¿sirve de algo esta capa?» no tiene respuesta. > **Lo que puede degradarse sin riesgo es lo que más fácilmente se degrada sin que nadie lo note** > > El asesor está diseñado para que «fallo = como si esta capa no existiera», así que cuando el servicio del modelo no arranca, el Bot se comporta exactamente igual que con reglas puras y desde fuera no se nota nada. Al cerebro de referencia le pasó que, durante un día entero, los asesores de sus 6 instancias no pudieron conectarse y nadie se dio cuenta. Publica siempre «la hora del último éxito» y «el motivo del último fallo»: la página «Asesor de estrategia» de la [consola Farsight](https://war3ai.com/es/docs/console/) sirve justo para eso. ## Impleméntalo en tu propio Bot Aquí tienes un esqueleto mínimo que funciona con cualquier API compatible con OpenAI (LM Studio, Ollama o una API en la nube) y solo usa la biblioteca estándar: ```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 / cualquier servicio compatible con OpenAI MODEL = "your-model" POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"} SYSTEM = """Eres un coach de economía de Warcraft III: solo te ocupas de la economía y la estrategia, no del micro. Emite solo JSON con campos fijos: {"diagnosis": una frase, "workers": {"gold": entero, "lumber": entero}, "priority": [códigos de cuatro caracteres, máximo 4, solo de allowed], "posture": uno de los seis, "avoid": [máximo 2]} Habla solo a partir de los datos de la partida que se te dan; no inventes nada que no esté en los datos.""" 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 # segundos de juego: las decisiones económicas se miden en minutos, no hace falta preguntar cada tick 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: # la instantánea se lee en el hilo principal; el hilo de fondo no toca 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 # se cuenta y se descarta, no en silencio posture = "hold" self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))), # acotación "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: # timeout / respuesta sin sentido: como si esta capa no existiera 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 {} # las sugerencias demasiado viejas no se usan # ↓ capa de reglas: con plan vacío se siguen las reglas por defecto; con plan solo se ajustan reparto, prioridades y postura; las órdenes concretas las siguen dando las reglas ... ``` ## Cómo elegir el modelo | Situación | Recomendación | |---|---| | Local, con prisa | Los modelos MoE (en cada paso solo activan una pequeña parte de los parámetros) son mucho más rápidos que un modelo denso del mismo tamaño. El cerebro de referencia usa Qwen3.6-35B-A3B (LM Studio, Q4): mediana de **1.09 s**, la más lenta 1.45 s, y 5/5 respuestas se pueden pasar directamente a `json.loads` | | Local, modelo que «piensa» | **Tienes que desactivar la fase de pensamiento**; si no, todos los tokens se van en pensar y no sale ni un solo JSON. LM Studio ignora `/no_think`; el cerebro de referencia usa `/v1/completions`, arma el ChatML a mano y prerrellena un `` vacío y una `{` | | Modelo en la nube | La latencia suele ser mayor, pero esta arquitectura ya es asíncrona; las decisiones económicas se miden en minutos, así que unos segundos de latencia son aceptables | > **Nota** > > El mismo modelo también puede poner voz a las unidades: consulta [Bocadillos y modelos locales](https://war3ai.com/es/docs/speech/). Si quieres que el modelo dé órdenes directamente tick a tick (en lugar de hacer de asesor), espera a la pasarela JSON de la [Arena](https://war3ai.com/es/arena/). --- # El LLM usa herramientas directamente (MCP) > tools/war3_mcp.py es un servidor MCP. Conéctalo a Claude Code, Claude Desktop o cualquier cliente compatible con MCP y el LLM podrá ver la partida, dar órdenes, hablar con el jugador en pantalla, preguntarle con tarjetas y hacer capturas de la imagen, sin escribir código antes. `tools/war3_mcp.py` es un **servidor MCP** (stdio). Claude Code, Claude Desktop, frameworks de agentes con modelos locales: cualquier cliente compatible con MCP puede conectarlo, y el LLM podrá **directamente** ver la partida, dar órdenes, hablar con el jugador en la pantalla del juego, hacerle preguntas y hacer capturas de la imagen, sin escribir código antes. Además de escribir Bots, hacer de asesor o poner voz a las unidades, esta es otra forma de conectarlo: **el propio LLM es quien usa las herramientas**. ## Conectarlo ```bash claude mcp add war3 -- python \tools\war3_mcp.py --inst 9 # Claude Code; cambia por tu carpeta de openwar3 ``` En otros clientes, escribe la configuración con este formato: ```json {"mcpServers": {"war3": {"command": "python", "args": ["\\tools\\war3_mcp.py", "--inst", "9"]}}} ``` Solo se conecta al juego la primera vez que se llama a una herramienta, así que el juego se puede abrir después; si el juego se cierra y se vuelve a abrir, la siguiente llamada se reconecta sola. Añade `--role` para limitar lo que puede hacer el LLM: | Rol | Qué puede usar | |---|---| | `dev` (por defecto) | Todas las herramientas, incluida `war3_jass` | | `player --player N` | Solo puede mandar las unidades del jugador N y solo ve lo que está en su visión (modo justo); sin JASS | | `observer` | Solo lectura; no puede dibujar en pantalla ni hacer hablar a las unidades; el runtime rechaza directamente los comandos que envíe | El rol `player` tiene las mismas limitaciones que en la [pasarela](https://war3ai.com/es/docs/gateway/): no puede terminar la partida, cambiar la velocidad ni pausar, no tiene acceso a las interfaces que dejan ver las cartas de los demás, y las consultas que llevan número de jugador solo pueden consultar el propio. Algunos límites: el resultado de una herramienta tiene como máximo 200 000 caracteres; si se pasa, se recorta y se indica cómo acotar la consulta; `war3_ask_player` espera como máximo 120 segundos; el `scale` de las capturas va de 0.1 a 1. ## Herramientas | Herramienta | Qué hace | |---|---| | `war3_overview` | La partida en una página: tiempo, recursos, comida, número de unidades propias por tipo, héroes (vida, maná, nivel, enfriamientos), tipos de unidades enemigas visibles, producción. **Llámala primero** | | `war3_units` | Lista de unidades (`owner` puede ser me / enemy / creep / all; `types` filtra); el `addr` sirve para dar órdenes | | `war3_events` | Lo que ha pasado desde la última llamada: muertes, subidas de nivel, hechizos, producción terminada, chat, botones que pulsó el jugador… (por defecto quita los tipos más ruidosos) | | `war3_call` | Llama a cualquier interfaz pública (`move`, `attack_move`, `train`, `build`, `cast`, `learn`, `ui.button`, `canvas.text`…); las unidades se escriben `{"unit": addr}` | | `war3_api` | Busca interfaces por palabra clave en el nombre y la descripción | | `war3_toast` / `war3_say` | Una línea de texto en la parte de arriba de la pantalla / una frase sobre la cabeza de una unidad | | `war3_ask_player` | Muestra al jugador unas tarjetas de elección en el centro de la pantalla, espera a que pulse una y devuelve cuál eligió (puede pausar el juego) | | `war3_screenshot` | Captura de la imagen del juego (PNG; funciona aunque la ventana esté tapada y no roba el foco) | | `war3_jass` | Ejecuta un fragmento de JASS (solo dev; cambiar el mundo, solo en partidas de un jugador) | Lo que se puede hacer con esto: - **Acompañante / entrenador**: `war3_overview` para ver la partida y `war3_toast` para dar consejos en pantalla; - **Preguntar al jugador mientras juega**: `war3_ask_player` muestra tres tarjetas y se sigue la que pulse el jugador; - **Comentarista**: `war3_events` para leer lo que ha pasado y `war3_say` para que lo cuenten las propias unidades; - **Mandar directamente un ejército**: rol `player` + `war3_call`; solo puede mover sus propias unidades; - **Ajustar la interfaz mirando la imagen**: `war3_screenshot` hace una captura para comprobar si los botones que dibujó están bien colocados. ## Así es, más o menos, una conversación ```text Tú: Mira cómo va la partida y luego pregúntame en pantalla: ¿ahora expando, saco tropas o mejoro el ayuntamiento? → war3_overview {} ← La partida en una página: tiempo de juego, oro 500, comida 10/12, propias htow 1 · hpea 5 · Hpal 1, ningún enemigo a la vista, nada en producción → war3_ask_player {"question": "¿Siguiente paso?", "options": ["Sacar tropas", "Expandir", "Mejorar ayuntamiento"], "pause": true} ← {"picked": 1, "option": "Expandir"} Modelo: Has elegido expandir. Primero busco un campesino libre con war3_units y luego miro dónde está la mina de oro más cercana… ``` ## Medido 2026-09-25: - Con un cliente MCP propio conectado a una partida real, 7/7: handshake → listar herramientas (10) → `war3_overview` (`htow` 1, `hpea` 5) → `war3_units` → `war3_toast` → `war3_screenshot` (PNG de unos 200 000 bytes) → `war3_ask_player` (tres tarjetas; clic simulado en la segunda → `{"picked": 1, "option": "Expandir"}`). - Conectado de verdad a Claude Code 2.1: arranca el servidor por su cuenta, hace el handshake, el estado es `connected` y las 10 herramientas aparecen en su lista de herramientas como `mcp__war3__*`. ## Implementación - JSON-RPC 2.0 delimitado por saltos de línea (`initialize` / `tools/list` / `tools/call` / `ping`), versión del protocolo 2025-06-18, compatible con 2025-03-26 y 2024-11-05. - Los errores de las herramientas van dentro del resultado, como marca MCP (`isError: true`), sin cortar la conexión. - Comparte con la [pasarela](https://war3ai.com/es/docs/gateway/) la misma lista blanca de roles, el mismo formato de parámetros de unidad y la misma «partida en una página». - Los logs van a stderr; stdout solo lleva el protocolo. --- # Modelo mental > Instantánea, comando, recibo, evento, tick y lote. Con estos seis conceptos entenderás por qué la API tiene esta forma y cómo escribir código rápido. ## Instantánea: leer, sin esperas Cada **50 ms**, el runtime recorre el mundo entero en el hilo del juego y lo escribe en memoria compartida. Lo que devuelve `g.snapshot()` es un mundo **completo y coherente**: - 16 slots de jugador: oro, madera, comida, límite de comida, total recolectado, raza; - Hasta 1024 unidades: tipo, dueño, coordenadas, vida / maná (con sus máximos), orden actual y objetivo de la orden, **a quién ataca realmente** (objetivo de tarea), nivel / experiencia / puntos de habilidad del héroe, y quién puede verla; - Hasta 256 detalles de unidad: 12 habilidades (nivel, enfriamiento restante), 8 buffs, 6 casillas de inventario; - Objetos en el suelo, árboles (se refrescan cada 2 segundos), tabla de producción (progreso de entrenamientos / investigaciones / construcciones / mejoras), reloj del juego y hora del día dentro del juego. Leer una copia cuesta unos **0.4 ms** (parseo en Python) y no espera al hilo del juego. Así que: **lee todo lo que quieras**. Interfaces como `g.units()`, `g.my_army()`, `g.cooldown()` o `g.inventory()` salen todas de la misma instantánea; llamarlas muchas veces en un mismo tick no cuesta casi nada. > **Consejo** > > El periodo de publicación es configurable: `g.set_publish_period(ms)`, de 16 a 1000 milisegundos. Cada recopilación cuesta unos 0.5 ~ 0.9 ms en el hilo del juego, así que 33 ms no es problema. El valor es único para toda la máquina y gana el último que se escribe. ## Comando: escribir, en torno a un frame `g.move / attack / gather / build / train / cast …` se entregan al hilo del juego para que los ejecute. El runtime ejecuta por lotes los comandos enviados por los clientes dentro del **despacho de eventos** del hilo del juego, así que un comando espera alrededor de **un frame** (unos 0.1 ms si cae en una ráfaga de eventos; si no, espera al siguiente despacho). - Un comando acepta **una unidad o una lista**; las unidades de la lista reciben la orden juntas en el mismo frame; - `queue='after'` equivale a encolar con Shift: primero termina lo que está haciendo y luego hace esto; - En los comandos basta con pasar los objetos de unidad obtenidos de la instantánea; el SDK comprueba su identidad con el **par de handles** (las unidades nuevas reutilizan direcciones; los handles no se reutilizan). ## Recibo: todos los comandos tienen uno ```python r = g.build(worker, "hbar", x, y) if r: # el motor lo aceptó ... else: r.reason # 'rejected(金不够)' = "falta oro" r.verdict # 8 r.exec_us # microsegundos que tardó en ejecutarse en el hilo del juego ``` El recibo se lee en el **mismo frame**: la orden de la unidad antes y después de darla, el valor devuelto por la función del motor y el código de motivo de la comprobación de viabilidad. Responde a «¿aceptó el motor este comando y, si no, por qué?», pero **no responde** a «¿se consiguió al final?». Para eso, mira la instantánea y los eventos. Todos los códigos de estado y de motivo están en [Recibos y códigos de motivo](https://war3ai.com/es/docs/reason-codes/). ## Evento: qué ha pasado `on_event(g, ev)` se ejecuta antes de cada `on_tick` y te entrega, uno a uno, los eventos ocurridos desde el tick anterior: | Evento | Significado | |---|---| | `unit.appeared` / `unit.died` / `unit.removed` | Una unidad aparece, muere o desaparece (entrar en una mina de oro, ser convertida o que se pudra el cadáver también cuentan como desaparecer; no equivale a morir) | | `unit.damaged` / `order.changed` / `owner.changed` | Pierde vida, cambia de orden, cambia de dueño | | `hero.levelup` | Un héroe sube de nivel | | `item.appeared` / `item.removed` | Un objeto aparece en el suelo, o alguien lo recoge o lo usa | | `damage` | A nivel de motor: **cada golpe** de daño. Unidad de origen, tipo de ataque, tipo de daño, vida perdida real, daño antes de armadura | | `killed` | A nivel de motor: este golpe la mató; incluye al asesino | | `production.done` | Terminó un entrenamiento / investigación / construcción / mejora; incluye el código de cuatro caracteres y cuántos segundos de juego tardó. También llega el de los rivales | | `spell.cast` | Una unidad lanzó una habilidad: código de cuatro caracteres de la habilidad, nivel, segundos de enfriamiento, punto de lanzamiento | | `message` | Aparece una línea en un marco de mensajes de la pantalla: avisos del juego («Necesitas más granjas»), chat (`.chat` incluye quién habla y qué dice), mensajes del sistema | | `selection.changed` / `player.left` | Cambió la selección del jugador local / un jugador se fue o fue eliminado por derrota | | `game.started` / `game.ended` | Empieza una partida nueva / se sale de la partida | Los eventos de entrada, como clics en botones del lienzo, atajos de teclado o clics en el suelo, están en [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/). > **Atención** > > El flujo de eventos es **global**: incluye las producciones terminadas del rival y las muertes de creeps. Filtra por `ev.owner` o por el handle de la unidad. ## Tick: el ritmo del Bot Por defecto, `on_tick` se llama 5 veces por segundo (reloj real). Lo que tarda un tick es básicamente tu propio cálculo: la instantánea no espera y un comando tarda alrededor de un frame. Si un tick se pasa del periodo, el siguiente se aplaza automáticamente; no se acumulan. - **A velocidad 2×, no esperes según el reloj real.** Para esperar 3 segundos de juego, comprueba que `g.clock()` haya avanzado 3; no uses `sleep(1.5)`. - **No uses `sleep` dentro de `on_tick`.** Si necesitas «hacerlo dentro de un rato», apunta la hora de juego actual y compruébala en el siguiente tick. ## Lote: decenas de comandos, una sola espera Si en un tick tienes que dar muchos comandos, envuélvelos en `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") # al cerrar el bloque se envía todo el lote: se ejecuta en el mismo frame y se espera una sola vez al hilo del juego ``` - Dentro del bloque, los comandos devuelven `Pending`, que se convierte en recibo al cerrarse el bloque; leerlo antes lanza un error; - Si dentro del bloque se lanza una excepción, **se descarta el lote entero** (medio lote de comandos es más peligroso que no enviar nada); - Medido con 8 movimientos: uno a uno, 68 ~ 99 ms; en un lote, **6.5 ~ 10 ms**. La misma idea sirve para las consultas: `g.can_do_many([(u, code), ...])` y `g.tech_many([...])` preguntan muchas cosas de una vez. ## Leer lo que acabas de escribir En un mismo tick, la instantánea todavía no refleja el comando que acabas de dar (se pone al día en la siguiente publicación). Dos partes de tu lógica pueden pelearse por el mismo trabajador: una acaba de mandarlo a construir una granja y la otra, al mirar la instantánea, cree que sigue ocioso. `g.order_of(u)` resuelve esto: hasta que la instantánea se pone al día, usa la orden nueva que indica el recibo. **Para saber si una unidad está ociosa, usa `g.order_of(u)`, no `u.order`.** `g.idle_workers()` ya excluye a los que acabas de mandar a trabajar en este tick. ## Niveles de latencia | Nivel | Canal | Latencia | Para qué | |---|---|---|---| | 0 | Instantánea push + flujo de eventos | Leer una copia, unos 0.4 ms; datos nuevos cada 50 ms | Todas las interfaces de «observar» | | 1 | Carril rápido | Alrededor de 1 frame; mediana de 0.06 ms con 6 procesos en paralelo | Todos los comandos y consultas (por defecto en el SDK) | | 2 | Canal de control | 20 ~ 40 ms | Respaldo y unas pocas operaciones de interfaz (velocidad de juego, bocadillos, mensajes) | | 3 | [Pasarela](https://war3ai.com/es/docs/gateway/) (WebSocket / JSON) | Nivel 1 + alrededor de 1 ms | Cualquier lenguaje, navegadores, LLM, programas en otra máquina | En el [catálogo de la API](https://war3ai.com/es/api/), cada interfaz indica qué nivel usa. --- # Las quince reglas > Cada una se aprendió a base de errores en partidas reales. Repásalas al escribir un Bot y te ahorrarás la mayor parte de la depuración. > **Consejo** > > Dale esta página junto con [`api.json`](https://war3ai.com/es/api.json) a un LLM y el Bot que escriba dará muchos menos rodeos. ## Leer el estado ### 1. Lo que no se puede leer es `None`, no 0 `resources()`, `time_of_day()`, `production()` y `cooldown()` pueden devolver `None` (cargando, la unidad no tiene detalle, el edificio no está produciendo…). Compruébalo antes de usarlo: ```python res = g.resources() if res is None: return ``` ### 2. Identifica las unidades por handle, no por dirección Las direcciones se reutilizan para unidades nuevas: una dirección antigua puede acabar apuntando a una unidad recién creada. Para recordar una unidad entre ticks, guarda `u.handle` y recupérala con `g.unit(handle)`. ### 3. El flujo de eventos es global `production.done` y `unit.died` incluyen también los del rival y los de los creeps. Filtra por `ev.owner` (o por el handle del edificio): ```python if ev.kind == "production.done" and ev.owner == g.me(): ... ``` ### 4. Los trabajadores dentro de una mina de oro no están en el snapshot Cuando un trabajador entra en la mina desaparece del snapshot (`unit.removed`, no ha muerto). Para contar cuántos hay en cada mina, **lleva tu propia cuenta** y no la recortes según el snapshot; si no, mandarás más gente a minas que ya están llenas. ## Dar comandos ### 5. Recibo "aceptado" ≠ hecho El motor acepta al momento incluso un punto de construcción dentro de un bosque; falla cuando llega el trabajador. Las habilidades pueden interrumpirse. El efecto se ve en el snapshot y en los eventos: para construir, usa `build_near` (comprueba si aparecen los cimientos) y, para los hechizos, mira si `g.cooldown()` ha entrado en enfriamiento. ### 6. No se puede atacar lo que no se ve Un comando con objetivo sobre un enemigo en la niebla se rechaza con el código de motivo **1001**. Para perseguir a un enemigo en la niebla, usa `attack_move` hacia la última posición donde se le vio. ### 7. Da órdenes solo a las unidades ociosas Repetir el mismo comando a la misma unidad en cada tick la interrumpe: las tropas se quedan temblando en el sitio y el ciclo de recolección de los trabajadores vuelve a cero. Para saber si una unidad está "ociosa", usa `g.order_of(u)` (incluye lo que acabas de ordenar en este tick), no `u.order` del snapshot (el snapshot aún no se ha puesto al día). ### 8. Shift solo "inserta después de la orden actual" El motor no tiene "añadir al final": si envías B y C seguidos con `queue='after'`, obtienes A, C, B. Para recorrer una serie de puntos en orden, usa `g.path(units, puntos)`, y para que un trabajador construya varios edificios seguidos, `g.build_queue(worker, plan)`: ambos insertan en orden inverso y lo resuelven por ti. ### 9. Los comandos de un tick, en un solo lote Enviar decenas de comandos uno a uno supone esperar decenas de veces al hilo del juego; dentro de `with g.batch():` solo se espera una vez. ## Economía y producción ### 10. Como máximo 5 trabajadores por mina Más no aumenta los ingresos. El objetivo de trabajadores depende del número de minas: 5 al oro por mina, más unos cuantos a la madera. ### 11. Solo 1 en la cola de entrenamiento Llenar los 7 huecos deja el dinero bloqueado en la cola (en una prueba, el ayuntamiento tenía 4 campesinos en cola y 300 de oro bloqueado, y el inicio de partida fue mucho más lento). Pon el siguiente cuando `g.queue(b)` esté vacía. ### 12. Si falta comida, mira la tabla de producción `g.production(b).blocked` = hay algo en cola pero no ha empezado; casi siempre, por falta de comida. Va un paso por delante de "construir cuando la comida esté casi al límite": si pierdes un montón de tropas en un combate y la cola se atasca al reponerlas, lo sabes al instante. ### 13. El héroe es único, y sin la cola del ayuntamiento vacía no se sube de tier - Si el héroe muere, solo puedes usar `g.revive(altar)`; entrenar otro se rechaza (221). Revivir también requiere comida (un héroe ocupa 5). - No se puede mejorar el ayuntamiento mientras quede algo en su cola (código de motivo 185, "edificio ocupado"). ## Tiempo y espacio ### 14. A velocidad 2× no esperes según el reloj real Para esperar 3 segundos de juego, comprueba que `g.clock()` haya avanzado 3; no uses `sleep(1.5)`. Con la velocidad aumentada, el reloj del motor avanza más rápido que el reloj real. ### 15. En mapas con islas o bosques, no uses la distancia en línea recta Para elegir campamentos de creeps o expansiones, usa `g.path_distance(a, b)` (A* por tierra, rodeando bosques, acantilados y edificios); si no hay camino, devuelve `None`. El punto más cercano en línea recta puede estar al otro lado del mar. ## Una más: escribe en modo justo Con `--fair` solo se ven las unidades, objetos, producción y eventos dentro de tu visión; es la regla de la Arena. Si escribes ya en modo justo, no tendrás que cambiar nada para la [Arena](https://war3ai.com/es/arena/). Más detalles en [Modo justo](https://war3ai.com/es/docs/fair-mode/). --- # Modo justo > Un cliente inyectado en el juego puede leer todo el mapa. El modo justo hace que el Bot solo vea lo que está en su visión, igual que un jugador humano y que las reglas de la Arena. La capacidad de observación del proyecto nace de que "el cliente guarda el estado de todos los jugadores": el snapshot contiene todas las unidades del mapa, incluidos los enemigos en la niebla. Es muy cómodo para depurar, pero injusto en una competición. El **modo justo** hace que el SDK filtre según tu visión: ```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) # usando Game directamente run(MyBot, inst=5, fair=True) # o a través del ejecutor ``` ## Qué se filtra | Contenido | En modo justo | |---|---| | Unidades | Todas las propias + las enemigas y neutrales que tu bando ve en ese momento | | Objetos en el suelo | Solo los que están en la visión de tus unidades (la visión de día y de noche se calcula por separado, según la tabla de datos) | | Tabla de producción | Solo los edificios visibles (no ves qué está entrenando el rival) | | Eventos | Los tuyos; los visibles (o vistos en el último segundo); el daño que inflige tu bando | ## De dónde sale la visión - En el snapshot, cada unidad lleva una **máscara de visibilidad**: el bit p = el jugador p la ve en este momento (solo cuentan los jugadores 0 ~ 11 con unidades en el mapa; tus unidades siempre son visibles para ti). `u.visible_to(g.me())` la lee directamente, sin espera. - Para cualquier punto: `g.visible(x, y)` pregunta al motor (visible / niebla / máscara negra) por el carril rápido, y cada llamada tarda aproximadamente un fotograma. Si en un tick tienes que evaluar muchas unidades, usa `u.visible_to()` del snapshot en vez de llamar a `g.visible()` una por una. ## Memoria de enemigos: `last_seen` Un jugador humano recuerda "hace un momento vi un grupo de jinetes de lobo por allí". El SDK también lo recuerda por ti: en cada actualización del snapshot anota las unidades enemigas y los creeps que tu bando ve en ese momento (última posición, vida, hora); si los ve morir, los borra, y al cambiar de partida la memoria se vacía. ```python for u, t, age in g.last_seen(max_age=60): # enemigos vistos en los últimos 60 segundos de juego print(u.type, u.x, u.y, f"hace {age:.0f} s") heroes = [r for r in g.last_seen() if r[0].is_hero] # dónde estaban la última vez los héroes rivales camps = g.last_seen(owner="creep") # creeps vistos ``` En modo justo, esta es tu única "fuente de información sobre el rival", igual que para un jugador humano. En modo normal también se registra según la visión, así que puedes usar el mismo código. ## Con qué número de jugador das órdenes ```bash python tools/play.py --bot my_bot.py --player 1 --attach ``` `--player N` (o `Game(player=N)`) hace que el Bot dé órdenes como jugador N, y solo puede mandar las unidades del jugador N. Dos IA enfrentadas son simplemente dos canales como este en la misma partida. > **En local, la justicia es un acuerdo, no una barrera de seguridad** > > En tu propia máquina no hay forma de impedir que un programa lea todo el mapa. `--fair` es una restricción que te impones tú; en una competición de verdad, la garantía la da el proceso árbitro de la [Arena](https://war3ai.com/es/arena/): el Bot nunca toca la memoria compartida, solo recibe las observaciones que el árbitro filtra por visión, solo puede enviar acciones y cada acción valida antes la propiedad de la unidad. ## Por qué activarlo ya - En la Arena las reglas serán exactamente estas; si escribes en modo justo ahora, luego no tendrás que cambiar ni una línea; - Sin la información de todo el mapa descubres el nivel real de tu Bot (hoy el cerebro de referencia depende mucho de ella, por ejemplo del punto objetivo del capitán de la IA del juego; esto es justo una prueba de fuego); - El reconocimiento, la memoria y el juicio que escribas en modo justo son las capacidades de IA que de verdad valen. --- # Recetario de jugadas profesionales > La ventaja de los mejores jugadores viene sobre todo de decenas de pequeños hábitos. Esta página lleva las jugadas profesionales más comunes, una a una, a código del SDK; cada fragmento se puede copiar tal cual en on_tick. Convenciones: `g` es `Game`, `home` es nuestra base principal (`g.my_buildings({"htow", "hkee", "hcas"})[0]`), `now = g.clock()`. Los detalles de cada interfaz están en el [catálogo de la API](https://war3ai.com/es/api/), y los ejemplos completos que se pueden ejecutar, en [Bots de ejemplo](https://war3ai.com/es/docs/examples/). > **Consejo** > > Cuando le pidas a un LLM que añada una jugada, pégale esa receta junto con su código: funciona mucho mejor que pedirle que «juegue más como un profesional». ## I. Economía ### 1. Campesinos nunca ociosos, 5 por mina ```python for w in g.idle_workers(): # solo los ociosos (reasignar a uno ocupado interrumpe la recolección) 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]) ``` Lleva tú la cuenta de cuántos has mandado a cada mina (`crew`): los trabajadores que están dentro de una mina de oro no aparecen en la instantánea. El ejemplo completo está en `hello_bot.py`. ### 2. Solo 1 en la cola para no bloquear el dinero ```python for b in g.my_buildings({"hbar"}): if not g.queue(b): # encola el siguiente solo cuando la cola está vacía g.train(b, "hfoo") ``` ### 3. Que la comida nunca te bloquee ```python stuck = any(p.blocked for _b, p in g.all_production("me")) # hay cola pero no ha empezado = falta comida res = g.resources() if stuck or res["food_cap"] - res["food_used"] <= 6: g.build_near(builder, "hhou", home.x, home.y) ``` `blocked` se adelanta a «casi lleno»: si pierdes un montón de unidades en una pelea y la cola se atasca al reponerlas, lo sabes al momento. ### 4. Orden de construcción + volver a la mina al terminar (Shift) ```python spot = g.build_near(w, "hbar", home.x, home.y) if spot: g.gather(w, mine, queue="after") # al terminar vuelve a minar; no hay que buscarlo en el siguiente tick ``` Un campesino que construye varios edificios seguidos: `g.build_queue(w, [("hhou", x1, y1), ("hhou", x2, y2)])`. El dinero se descuenta al empezar cada obra. ### 5. Cuándo subir de tier; mejoras de ataque y armadura ```python if not g.queue(hall) and g.can_do(hall, "hkee") in (0, 220): # solo se puede mejorar con la cola del ayuntamiento vacía (si no, 185) g.upgrade(hall, "hkee") p = g.production(hall) # progreso de la subida de tier if p and p.kind == "upgrade": print(f"Faltan {p.remaining:.0f} s para el Torreón") 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. Expandirse: elegir la mina más cercana por distancia a pie ```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) # las minas en islas devuelven None -> van al final ``` ## II. Exploración e información ### 7. Ver qué está haciendo el rival ```python for b, p in g.all_production("enemy"): # qué entrenan / investigan / mejoran los edificios rivales que ves print(b.type, p.kind, p.queue, f"{p.progress:.0%}" if p.progress is not None else "atascado") ``` Combínalo con eventos: `ev.kind == "production.done" and ev.owner != g.me()` indica qué acaba de sacar el rival. ### 8. Recordar lo que has visto (niebla de guerra) ```python for u, t, age in g.last_seen(max_age=60): # enemigos vistos en los últimos 60 segundos de juego (última posición y vida) ... hero_seen = [r for r in g.last_seen() if r[0].is_hero] # dónde se vio por última vez al héroe rival ``` En modo justo es tu única fuente de información sobre el rival, igual que para un jugador humano. ### 9. Adónde va a atacar el ordenador (solo con la IA del ordenador) ```python plan = g.enemy_ai_plan(some_enemy_soldier) # adónde va su capitán del ordenador ``` El ordenador fija el punto objetivo antes de salir: lleva tus tropas allí con antelación. ## III. Creeping ### 10. Creepear de noche ```python if g.is_night(): # de 18:00 a 6:00: los creeps duermen (atacas primero sin que te rodeen) y la visión de todos se reduce ... wait = g.seconds_until(18) # segundos de juego que faltan para el anochecer (un día = 480 s) ``` ### 11. Atacar solo los campamentos que puedes ganar ```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) # cuánto tardamos en limpiar el campamento (estimación) theirs = g.time_to_kill(camp, weakest_of_mine) or 1e9 # cuánto tardan ellos en matar a nuestra unidad más débil if ours < theirs and g.reachable(center, camp[0]): g.attack_move(army, camp[0].x, camp[0].y) ``` Ejemplo completo: `_maybe_creep` en `micro_bot.py`. ## IV. Micro ### 12. Concentrar fuego: al que «muere más rápido», no al más cercano ```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) ``` Da la orden solo a quienes «no le están atacando ya» (`current_target`); no interrumpas a los que ya lo hacen. ### 13. Retirar las unidades heridas ```python for u in army: if u.hp < u.hp_max * 0.35: g.move(u, *toward(home, u, 500)) # retrocede 500 hacia casa; no la vuelvas a retirar en 3 segundos ``` Cómo saber si te concentran el fuego: en los eventos `damage`, una misma unidad recibe golpes de varias fuentes en poco tiempo = la están rodeando. ### 14. Mantener vivo al héroe y no regalar experiencia ```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")) # poción de curación: busca la casilla con inventory(h) ``` ### 15. Counters: la unidad adecuada contra el objetivo adecuado ```python s = g.stats(u) best = max(enemies, key=lambda e: s.dps_vs(g.stats(e))) # fusileros contra grifos (perforante contra armadura ligera ×2), grifos contra soldados (mágico contra armadura pesada ×2) ``` La tabla de counters sale de los datos del juego: `combat.damage_multiplier("pierce", "small") == 2.0`. ### 16. Flanqueos y rutas: rodear las torres ```python route = g.walk_path(army_center, target) # puntos de giro del camino terrestre más corto g.path(army, route, attack=True) # atacar-mover pasando por cada punto en orden ``` Para esquivar las torres, marca como intransitable la zona que las rodea en la cuadrícula de pathfinding y calcula de nuevo: ```python grid = g.grid().copy() for t in towers: grid.block_area(t.x, t.y, 800) # alcance de la torre 700 + margen route = grid.path((army_x, army_y), (target.x, target.y)) ``` ### 17. Todos los comandos de un tick en un solo lote ```python with g.batch(): g.attack(melee, target_a) g.attack(ranged, target_b) g.move(wounded, *home_xy) g.cast(hero, "thunderclap") ``` Decenas de comandos esperan una sola vez al hilo del juego (medido: 8 movimientos, de 68 ms a 6.5 ms). ### 18. Asedio: artillería contra el suelo ```python g.attack_ground(mortars, tower.x, tower.y) # morteros / catapultas disparan a una zona (detrás de un bosque, unidades invisibles) ``` ## V. Héroes ### 19. Tabla de habilidades ```python SKILLS = {"Hamg": ["AHwe", "AHbz", "AHwe", "AHbz", "AHwe", "AHmt"]} # Elemental de agua, Ventisca… y en el nivel 6 la definitiva, Teletransporte en masa info = g.hero_info(h) if info and info["skill_points"]: g.learn(h, SKILLS[h.type][learned_count]) # si la rechaza (nivel insuficiente para la definitiva), espera al siguiente nivel ``` ### 20. ¿Se lanzó el hechizo de verdad? ```python r = g.cast(h, "thunderbolt", target=enemy_hero) # en el siguiente tick: if g.cooldown(h, "AHtb"): # entró en enfriamiento = se lanzó de verdad; aceptado ≠ lanzado ... ``` ### 21. Comprar pociones y volver a la base ```python g.buy(shop, "phea") # el héroe tiene que estar junto a la tienda g.use_item(hero, slot, x=home.x, y=home.y) # pergamino de portal a la ciudad (usar objeto sobre un punto) ``` ## VI. Análisis de partidas - Escribe en el log las decisiones de cada tick (`print` va a la ventana de ejecución) y combínalo con `g.say(unidad, "¡Retirada!")` para verlo dentro del juego; - El evento `production.done` incluye «cuántos segundos tardó»: construye tu propia línea de tiempo de producción (en qué segundo sale el primer héroe, en qué segundo subes de tier) y compárala con la de los mejores jugadores; - Deja que el Agent analice sus propias partidas: consulta el informe de partida en [Iteración autónoma del Agent](https://war3ai.com/es/docs/agent-loop/). --- # Bots de ejemplo > Cuatro ejemplos de menor a mayor complejidad; todos se ejecutan tal cual y cada bloque de lógica corresponde a una capacidad del SDK. Además, un cerebro de referencia completo. Los ejemplos están en `brains/examples/`; cada uno hereda del anterior y solo añade lo nuevo. Te recomendamos leerlos en orden: | Ejemplo | Qué aprendes | Cómo ejecutarlo | |---|---|---| | `hello_bot.py` | Recolección (5 por mina; con la mina llena, a talar), entrenar trabajadores (solo 1 en cola), construir edificios de comida, retomar cimientos abandonados; funciona con las cuatro razas | `python tools/play.py --bot brains/examples/hello_bot.py` | | `rush_bot.py` | Cuartel y altar (si faltan, se construyen con `build_near`), el héroe primero (si muere, se revive), aprender habilidades cuando hay puntos, reunir una oleada y avanzar atacando | `… --bot brains/examples/rush_bot.py` | | `macro_bot.py` | Orden de construcción + volver a la mina al terminar (Shift), cubrir la comida en cuanto se bloquea, 1 en la cola del cuartel, mejoras de ataque y armadura, subir de tier y tropas avanzadas, elegir objetivo por **distancia real por tierra** y seguir la ruta | `… --bot brains/examples/macro_bot.py --speed 200` | | `micro_bot.py` | Sobre la macro, toma el control del combate: concentrar fuego en lo que antes muere, retirar heridos, salvar al héroe, elegir de noche campamentos de creeps asumibles, volver a defender si el enemigo llega a la base; los comandos de un tick van en un solo lote | `… --bot brains/examples/micro_bot.py --fair` | > **Nota** > > Los comentarios de `hello_bot` y `rush_bot` recogen trampas encontradas en partidas reales, como "elegía siempre al primer trabajador para construir y las 3 granjas se quedaron en cimientos a medias" o "las coordenadas fijas del cuartel caían justo en un bosque y en 3 minutos no se construyó ninguno". Leer los comentarios enseña más que leer el código. ## hello_bot: economía ```python # raza -> (trabajador, ayuntamientos, edificio de comida) 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 # como máximo 5 trabajadores por mina (más no aumenta los ingresos) LUMBER_CREW = 5 # leñadores: 5 al oro por mina + estos a la madera = objetivo de trabajadores ``` Tres cosas: los trabajadores ociosos van al oro (el Bot lleva su propia cuenta de cuántos hay en cada mina y, si está llena, los manda a talar); si faltan trabajadores, entrena más (solo 1 en la cola); cuando la comida está casi al límite, busca un trabajador que no esté construyendo y levanta un edificio de comida junto al ayuntamiento (con Humanos y Orcos, además, manda a alguien a terminar los cimientos abandonados). ## rush_bot: tropas y ataque Sobre `hello_bot` añade tres cosas: si no hay cuartel ni altar, los construye; el altar saca un héroe (**si muere, primero se revive**: el héroe es único) y aprende habilidades cuando tiene puntos; al reunir 8 unidades, todas avanzan atacando hacia el ayuntamiento enemigo y, si quedan muy dañadas, vuelven a casa para reagruparse. Solo da órdenes a las unidades ociosas, para no interrumpir el combate tick tras tick. ## macro_bot: fundamentos de macro ```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"]), ... } ``` Lo que un jugador profesional hace en cada partida, cada punto ligado a una capacidad del SDK: tabla de orden de construcción + `gather(..., queue="after")` para volver a la mina al terminar; `production().blocked` para detectar el bloqueo por comida; `g.queue` para que el cuartel tenga solo 1 en cola; `can_do` para preguntar al motor si se puede investigar el siguiente nivel de ataque o armadura; subir de tier y tropas avanzadas (lección de una partida real: se quedó en tier 1 y a los 23 minutos lo arrasaron los caballeros y jinetes de grifo de un rival en tier 3); elegir objetivo con `path_distance` y seguir los puntos de giro con `path()`. ## micro_bot: cuando empieza la pelea ```python def _fight(self, g, army, foes, home, now): ... visible = [e for e in foes if e.visible_to(me)] # los objetivos no visibles se rechazan (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)) # el que antes muere, no el más cercano 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) ``` En partida real: 5 minutos, 1497 ticks, 3023 comandos, 0 errores. ## Cerebro de referencia: una IA completa `brains/xwar3/` es una IA completa que expande, hace creeping y ataca, organizada en tres capas: | Capa | Ubicación | Ritmo | Qué hace | |---|---|---|---| | Capa de estrategia | `strategy/` | Segundos | Selección y cambio entre varias estrategias al estilo AMAI, tablas de construcción, tropas de counter, elección de héroes; [asesor de estrategia LLM](https://war3ai.com/es/docs/llm-coach/) opcional | | Capa de reflejos | `reflex/` (4 procesos independientes) | Del orden de 100 ms | Protección, hechizos, concentrar fuego, recoger objetos | | Modelo de victoria | `worldmodel/` | — | ¿Se puede ganar este combate? (subconjunto de inferencia) | Varios procesos comparten las unidades mediante una **tabla de reclamación**, y la prioridad decide quién manda: control manual 95 > protección 90 > esquivar habilidades 85 > hechizos 80 > recoger objetos 70 > … > estrategia 50 > asignación de trabajadores 45. Tu propio Bot figura en la tabla como `bot`, con prioridad 50 por defecto. > **Atención** > > El cerebro de referencia usa directamente la capa baja del SDK (`w3cmd` / `act`) y depende mucho de la información de todo el mapa. Sirve como referencia de "ideas", pero no conviene que un LLM lo copie tal cual. Necesita los datos de AMAI: la primera vez que `start.bat` despliega el entorno, los descarga del repositorio público de AMAI y los genera (AMAI tiene una licencia propia; lo generado no entra en git; si falla, reinténtalo con `start.bat setup`). La forma más sencilla de arrancar el cerebro de referencia es la [consola Farsight](https://war3ai.com/es/docs/console/): en la página "Instancias y partida", marca el número de instancia y pulsa "Iniciar prueba". --- # Depuración y rendimiento > Por qué un tick va lento, por qué un comando no surtió efecto, por qué el juego no se mueve. Diagnostica por síntomas y confírmalo con los scripts de verificación en vivo incluidos. ## Mira el recibo El recibo de cada comando es la primera pista: ```python r = g.cast(hero, "blizzard", x=tx, y=ty) if not r: print(r.reason, r.verdict) # rejected(…) y el código de motivo print(r.exec_us, r.engine_us) # microsegundos en el hilo del juego / de ellos, cuánto tardó la propia función de órdenes del motor ``` Lo normal es que un comando tarde entre unos microsegundos y unos cientos de microsegundos en el hilo del juego. Al terminar un bloque de lote, `g.last_receipts` contiene el recibo de cada comando del lote. ## Míralo en el juego ```python g.say(unit, "¡Retirada!") # burbuja de chat sobre la unidad (no afecta al juego) g.message("Voy a hacer creeping") # una línea en el área de mensajes de abajo a la izquierda (solo se ve en tu equipo) ``` Lo que imprimas con `print` aparece en la terminal donde se ejecuta el Bot. Imprimir las decisiones clave de cada tick, junto con las burbujas, es mucho más rápido que leer el código. ## Un tick va lento Primero comprueba si es uno de estos casos: | Causa | Solución | |---|---| | Enviar los comandos uno a uno, cada uno esperando un fotograma | Envuélvelos en `with g.batch():`; decenas de comandos esperan una sola vez | | Llamar a `g.visible()` / `g.can_do()` una y otra vez (cada llamada va por el carril rápido y espera un fotograma) | Para la visibilidad, usa `u.visible_to()` del snapshot; para la viabilidad, pregunta en lote con `g.can_do_many([...])` | | `sleep` o esperas dentro de `on_tick` | Anota el tiempo de juego y vuelve a comprobarlo en el siguiente tick | | Recalcular cosas caras en cada tick (rutas, barridos de todo el mapa) | Cachea el resultado y recalcula cada varios ticks. `g.grid()` trae una caché de 2 segundos, y los niveles de tecnología de `g.stats()` se cachean 5 segundos | ## El juego no se mueve / el Bot nunca entra en la partida | Síntoma | Causa probable | |---|---| | Se queda siempre "esperando a la partida" | Número de instancia incorrecto, o la ventana del juego está **minimizada**: al minimizarla, la simulación se detiene (el reloj no avanza) | | El juego corre, pero las órdenes del Bot no hacen nada | Estás dando órdenes a unidades ajenas (recibo `not_owner`), o el Bot se conectó con el rol observer (`forbidden`) | | Comandos `held` | Una capa de mayor prioridad retiene esa unidad (la capa de reflejos del cerebro de referencia, las órdenes manuales de la consola) y no se enviaron | | Se pueden dar comandos en pausa | Es normal: en pausa el reloj del motor se detiene, pero el despacho de eventos sigue funcionando y los comandos se ejecutan | ## Conéctate y mira el estado ```bash python -m openwar3 status --inst 5 ``` Muestra el estado de la conexión: pid del juego, ciclo de publicación del mundo y tiempo de cada recogida, contadores del carril rápido, si hay partida en curso, número de unidades y reloj del juego. ## Scripts de verificación en vivo Abre una instancia de prueba y comprueba, punto por punto, que las capacidades del SDK funcionan en tu máquina: ```bash python tools/sdk_live_check.py --inst 20 # todo python tools/sdk_live_check.py --inst 20 --only prod # solo una sección ``` Secciones: lotes, tiempo, producción, órdenes en cola, estadísticas de combate, rutas, modo justo. Cada sección da comandos en una partida real, lee el efecto e imprime cuántas comprobaciones pasan. Las pruebas offline no necesitan abrir el juego: ```bash python tools/run_tests.py ``` ## Cosas que "parecen un bug" - **El recibo de construcción fue aceptado, pero nunca aparecen los cimientos**: el motor acepta al momento incluso un punto dentro de un bosque; falla cuando llega el trabajador. Usa `build_near`, que hace el seguimiento y veta durante un tiempo los puntos que fallan. - **El recibo del hechizo fue aceptado, pero no se lanzó**: lo interrumpieron o faltaba maná. En el tick siguiente, comprueba si `g.cooldown()` ha entrado en enfriamiento. - **La orden de ataque fue aceptada, pero las tropas atacan a otro**: para atacar un objetivo concreto, usa `g.attack(unidad, enemigo)` (semántica de clic derecho). La orden de ataque en bruto sobre un objetivo solo cambia la orden sin fijar el objetivo, y la unidad acaba atacando a otra cosa cercana. - **El número de trabajadores no cuadra**: los trabajadores que están dentro de la mina de oro no aparecen en el snapshot. - **No puedo entrenar al héroe que murió**: el héroe es único; usa `g.revive(altar)`. Revivir requiere comida y solo es posible unos 3 segundos de juego después de su muerte. --- # Compañero para RPG > Dale al jugador un compañero IA en mapas de RPG o personalizados: te sigue, te ayuda contra los monstruos, te cura cuando vas mal de vida y charla contigo. Cuatro modos; hereda una clase, cambia unas cuantas propiedades y ya tienes tu propio compañero. No todo son enfrentamientos. En mapas de RPG y personalizados puedes llevar tu propio **compañero IA**: te sigue, te ayuda contra los monstruos, te cura cuando te queda poca vida y, cuando no pasa nada, charla un poco contigo; sus frases pueden salir incluso de un LLM local. **Cómo usarlo lo decides tú.** Está dividido en tres capas de API, de abajo arriba, y cualquiera de ellas se puede usar directamente: | Capa | Qué es | Ideal si | |---|---|---| | **Canal JASS** `g.jass` | Las 1291 funciones JASS que tienen los autores de mapas, llamadas directamente por su nombre (crear unidades, fijar aliados, dar objetos, cambiar nombres, mostrar texto, revivir héroes…) | Quieres crear tu propia mecánica de juego | | **API de conveniencia** | `g.spawn`, `g.set_alliance`, `g.player_slots`, `g.show_text`, `g.map_data`: las tareas más habituales, ya empaquetadas | Escribes tus propios scripts de ayuda | | **Framework de compañero** | `openwar3.companion.Companion` + `openwar3.talk.Talk`: hereda, cambia unas cuantas propiedades y tienes un compañero que te sigue, lucha a tu lado, te cura y charla | Quieres un compañero | > **Atención** > > Solo para partidas **sin conexión o en LAN creadas por ti**. Crear unidades o fijar aliados cambia el mundo de forma unilateral desde tu equipo: en partidas de un jugador (contra el ordenador) no hay problema; en multijugador desincronizaría a los demás jugadores, así que en esas partidas el canal JASS solo deja pasar funciones de solo lectura y el compañero pasa automáticamente a "solo hablar". ## Lo más rápido: activarlo con un clic en Farsight 1. **Elige el mapa**: página "Instancias y partida" de Farsight → "Siguiente partida" → Mapa, y elige un mapa de RPG (aparecen los de `Scenario` y `Download` dentro de `Maps` en el directorio del juego, p. ej. `(4)WarChasers`). 2. **Elige el esquema**: en el desplegable "Esquema de IA" de la tarjeta de la instancia, selecciona **Ejemplo de compañero (buddy)** → "Seleccionar". 3. **Iniciar prueba**: cuando el juego arranque, **juegas tú en la ventana del juego**. El compañero, un paladín llamado "Luz", aparecerá a tu lado. También puedes hacerlo desde la línea de comandos: ```bash python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "\Maps\Scenario\(4)WarChasers.w3m" ``` `--rpg` (en el manifiesto del esquema, `"judge": false`) indica que el resultado no se decide con las reglas de las partidas competitivas: en un RPG los héroes muertos pueden revivir y tampoco existe "pierdes si te quedas sin edificios". Muchos mapas de RPG se quedan en "Pulsa cualquier tecla para continuar" al terminar de cargar; cuando el SDK detecta que "está en partida, pero el reloj de juego sigue en 0", pulsa espacio por su cuenta (`g.press_to_continue()`, que solo envía un mensaje de tecla a la ventana del juego y no le roba el foco). ## Escribe tu propio compañero ```python from openwar3.companion import Companion from openwar3.talk import Talk class MyBuddy(Companion): mode = "ally" # modo, ver la tabla de abajo unit = "Hpal" # qué crear: cualquier código de cuatro caracteres, también los propios del mapa nickname = "Luz" heal = ("holybolt", "AHhb", 0.55) # (orden del hechizo, habilidad que aprender, cura si la vida del amo baja de este valor); None = no cura follow_distance = 350 talk = Talk(persona="Un joven paladín alegre al que le encanta animar a su amo") ``` ### Cuatro modos | mode | Quién es el compañero | Notas | |---|---|---| | `ally` (por defecto) | Ocupa una ranura de jugador libre y es tu **aliado** | Tiene su propio color y nombre (el marcador y el panel de aliados muestran `nickname`); no puedes darle órdenes, lucha por su cuenta. El framework configura automáticamente la alianza + visión compartida | | `own` | Se crea **a tu nombre** | Puedes darle órdenes a mano cuando quieras; cuando no lo controlas, la IA lo maneja por ti | | `adopt` | Toma el control de una unidad **que ya existe** en el mapa | Sobrescribe `adopt(g)` para que devuelva esa unidad (la mascota o el acompañante que te da el mapa) | | `voice` | No crea ninguna unidad, **solo habla** | Te hace compañía y te avisa de cosas; no cambia el mundo, así que también funciona en multijugador | Si no hay ranuras libres, `ally` pasa automáticamente a `own`; en multijugador, o si no se puede crear la unidad, pasa automáticamente a `voice`. > **Nota** > > En el modo `ally`, el compañero se queda con "la mejor unidad que tenga esa ranura en ese momento" (los héroes primero), en lugar de aferrarse a una unidad concreta. En las pruebas, un mapa trató al compañero como a un jugador humano: borró el paladín y le dio un héroe del mapa. El compañero tomó directamente el control de ese héroe y también aprendió las habilidades que el mapa le había asignado. Si el héroe muere, primero intenta revivirlo en el sitio; si lo revive el propio mapa, sigue usándolo. ### Qué hace en cada tick Comprueba en este orden y hace lo primero que se cumpla: | Orden | Acción | Condición | Ajustable | |---|---|---|---| | 1 | Retirarse | Su propia vida baja del 25 % y hay enemigos cerca: se retira detrás del amo | `retreat_at` | | 2 | Curar | La vida del amo baja del umbral, la habilidad está lista y la distancia es menor de 900 | `heal` (None la desactiva) | | 3 | Apoyar | Hay enemigos alrededor del amo: **el que ataca al amo > al que ataca el amo > el más cercano** | `assist_radius`, o sobrescribe `pick_target` | | 4 | Seguir | Si se aleja demasiado del amo, lo alcanza; si se aleja mucho, vuelve corriendo sin entretenerse en combates | `follow_distance`, `leash` | | 5 | Charlar | Sin enemigos cerca, suelta una frase cada 1 ~ 2.5 minutos | Tabla de frases | Quién es "enemigo" se decide según las relaciones de alianza del juego (se refrescan cada 20 segundos). Los mapas de RPG suelen tener varios bandos aliados, así que no basta con tratar como enemigo a "todo jugador que no sea yo". Hooks que puedes sobrescribir: `find_master` (quién es el amo; por defecto, el héroe de mayor nivel del jugador local), `adopt`, `pick_target`, `on_poke` (el amo hace clic derecho sobre el compañero), además de `on_start` / `on_tick` / `on_event` / `on_end` del Bot. El número de curas, apoyos, bajas, seguimientos, retiradas, frases y resurrecciones se guarda en `self.stats` y se imprime al terminar. ### Cómo llamarlo - **Comandos de chat**: escribe en el chat `-follow` (sígueme), `-stay` (quédate aquí), `-heal` (cúrame ya) o `-hi` (saluda). Para cambiar la lista de comandos, cambia `commands`; para cambiar cómo reacciona, sobrescribe `on_command`. - **Clic derecho sobre el compañero**: dispara `on_poke`. En el ejemplo, si el amo no tiene la vida al máximo, le da una cura; si no, dice algo. - **Diálogo con retrato**: el saludo, cuando cae el amo, cuando sube de nivel y cuando vuelve el compañero se dicen con el diálogo con retrato del propio juego (el retrato de abajo cambia al del compañero y aparecen subtítulos en pantalla); el resto sale en burbujas sobre su cabeza. - **Panel de estado**: un panel en el lado izquierdo de la pantalla con la barra de vida del compañero, qué está haciendo, su ánimo (feliz / emocionado / nervioso / asustado / triste) y cuántas bajas y curas lleva. Se dibuja con el [lienzo](https://war3ai.com/es/docs/canvas/), así que también es seguro en multijugador. ### Hablar, y el LLM local `Talk` elige frases según los eventos y las muestra en una burbuja sobre la cabeza; en el modo `voice`, o cuando no puede mostrar la burbuja, aparecen en la esquina inferior izquierda de la pantalla. Cada frase se guarda también en el log del esquema, para poder consultar después qué dijo. | Evento | Cuándo | Evento | Cuándo | |---|---|---|---| | `hello` | Al llegar | `master_low` | Al amo le queda poca vida | | `poke` | El amo le hace clic derecho | `master_levelup` | El amo sube de nivel | | `fight` | Empieza un combate | `master_died` / `master_back` | El amo cae / revive | | `kill` | Mata a un monstruo (dice el nombre del monstruo) | `buddy_low` / `buddy_died` / `buddy_back` | Al compañero le queda poca vida / cae / vuelve | | `healed` | Ha curado al amo | `idle` / `item` | Charla / recoge un objeto | En las frases puedes usar los marcadores `{master}`, `{me}`, `{map}`, `{enemy}`, `{level}` e `{item}`; para cambiar las frases, edita `talk.lines`; el tiempo de espera entre frases está en `talk.cooldown`. **Conectar un LLM local**: `Talk(llm=LocalLLM(url, model))`; sirve cualquier API compatible con OpenAI (LM Studio, Ollama…). El modelo responde en un hilo en segundo plano y la frase solo se dice cuando llega la respuesta; si el modelo no está en marcha, tarda demasiado o falla, se dice una frase fija, así que nunca bloquea el juego. Las peticiones solo van a la dirección local que indiques, y lo que se envía es lo que pasa en la partida (cómo se llama el amo, qué monstruo ha matado). ## Nombres de unidades en mapas personalizados Las unidades, objetos y héroes de los mapas de RPG suelen ser creaciones del propio mapa (con códigos de cuatro caracteres como `HC07` o `I00A`) y no aparecen en la tabla de nombres integrada. `g.map_data` lee directamente el archivo del mapa de la partida actual: ```python md = g.map_data md.name_of("HC07") # 'Optimus Primo': manda el nombre modificado por el mapa md.hero_names("HC07") # lista de nombres propios del héroe md.hero_skills("OC10") # habilidades que el mapa asigna a este héroe md.tooltip("I00A") # texto de descripción ``` Los mapas protegidos u optimizados (muchos RPG populares) no incluyen los archivos estándar de datos de objetos del editor, así que los nombres se leen de los datos de texto del mapa. En las pruebas, los 38 mapas de RPG / personalizados de este equipo se analizaron sin errores, y en 37 se obtuvieron los nombres de las unidades. ## Compartirlo como esquema Un compañero no es más que una subclase de `openwar3.Bot`, así que puedes convertirlo en un [esquema de IA](https://war3ai.com/es/docs/schemes/) y compartirlo. En el manifiesto se añaden dos campos: ```json {"id": "my-buddy", "name": "Mi compañero", "entry": "my_buddy.py", "fair": false, "judge": false} ``` `"fair": false`: necesita el canal JASS (crear unidades, fijar aliados); `"judge": false`: el resultado no se decide con las reglas de las partidas competitivas. ## Registro de pruebas 24 de septiembre de 2026, instancia de prueba, mapa WarChasers, velocidad 2×: - Canal JASS: las 18 comprobaciones pasaron: ranuras de jugador, conversión de ida y vuelta entre unidades y handles, valores de retorno de tipo real, parámetros de cadena, crear unidades en una ranura libre, fijar aliados, cambiar nombres, borrar unidades; las llamadas desde un carril de jugador y las que tenían un número de parámetros incorrecto se rechazaron correctamente. - Compañero: pulsó por su cuenta "Pulsa cualquier tecla para continuar" → apareció junto al amo y lo saludó → lo siguió hasta el círculo de poder para elegir héroe; el mapa le dio un héroe y tomó su control → lo siguió (a 200 ~ 400 del amo) → luchó contra monstruos y, al matar uno, dijo "¡Genial!" → se retiró con poca vida → murió, el mapa lo revivió y siguió acompañándolo. ## Lo que falta 1. **No puede leer cualquier texto que el jugador escriba en el chat.** Los comandos de chat fijos ya funcionan; para que el compañero charle contigo libremente de verdad, hace falta obtener el propio texto. 2. **El compañero no entiende la mecánica de cada mapa concreto** (misiones, tiendas, historia). Hace lo genérico: seguir, apoyar y curar; para que entienda un mapa concreto, escríbelo en tu subclase para ese mapa: `g.map_data` te da los nombres y `g.jass` puede llamar a cualquier función. Esa es justo la parte que queda en tus manos. --- # Lienzo > Dibuja sobre la imagen del juego cuadros de texto, paneles, barras de progreso, imágenes, círculos pegados al suelo y rutas con flecha. El runtime lo dibuja él mismo en cada fotograma sin cambiar el estado del juego, así que también es seguro en multijugador; desde Python, por HTTP o escribiendo directamente en memoria compartida. Un programa externo puede dibujar sobre la imagen del juego **cuadros de texto, paneles, barras de progreso, imágenes, círculos en el suelo y rutas en el suelo (con flecha)**, y el runtime los dibuja él mismo en cada fotograma. Sirve para tu propio HUD, líneas de ayuda, avisos, anotaciones didácticas o paneles de información para directos. ## Lienzo o funciones visuales de JASS: cuál elegir | | Lienzo (esta página) | [Funciones visuales de JASS](https://war3ai.com/es/docs/jass/) | |---|---|---| | Quién dibuja | El runtime, por su cuenta | El propio juego (texto flotante, efectos, paneles, diálogo con retrato…) | | Multijugador | **Seguro**: solo dibuja en tu pantalla, no crea objetos del juego ni cambia su estado | Solo en partidas de un jugador | | Estilo | Libre: fuentes propias (también con caracteres chinos), esquinas redondeadas, semitransparencia, bordes, cualquier color, imágenes locales | Estilo nativo del juego | | Seguir algo | Unidades, coordenadas del mundo, posiciones de pantalla; los círculos en el suelo siguen el relieve del terreno | Depende de la función | | Coste | Medido: 0.2 ~ 0.35 ms por fotograma (9 elementos) | Unos 13 ms por llamada | Se pueden usar las dos a la vez: JASS para los efectos con estilo nativo, el lienzo para paneles propios, líneas de ayuda y avisos. ## Python ```python c = g.canvas # la primera vez, el runtime instala el hook de dibujo (unos 0.1 s) c.text("title", "Hola, esto es el lienzo", screen=(40, 110), color=(255, 220, 80), bg=(0, 0, 0, 170), border="#C49C40", size=22, bold=True) c.panel("status", "Compañero · Luz", ["Ánimo: feliz", "Bajas: 12"], screen=(16, 330)) c.bar("hp", 0.62, unit=hero, lift=260, width=100, height=12, color=(80, 220, 80), text="62%") # sigue a la unidad c.text("tag", "¡El jefe va a lanzar su ataque definitivo!", 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) # zona peligrosa en el suelo c.circle("aura", hero, 450, color=(80, 200, 255, 220)) # círculo que sigue a la unidad 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 solo borra lo que dibujaste tú c.expire("tag", 5) # desaparece por sí mismo a los 5 s with c.batch(): ... # cambia muchos elementos y escribe una sola vez en memoria compartida c.stats() # si drawnFrames sube, de verdad se está dibujando ``` Cada elemento se identifica con una `key`: dibujar otra vez con la misma key lo actualiza. **Clicables**: añade `clickable=True` a un cuadro de texto o a un panel (el color al pasar el ratón se fija con `hover=`); al hacer clic llega un `ui.click` al flujo de eventos, con `ev.key` igual a esa key, y el juego no recibe ese clic. Para botones, tarjetas de elección, atajos de teclado y clics en el suelo ya listos, ver [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/). **Posición** (una por elemento): - `screen=(x, y)`: píxeles de pantalla; los negativos cuentan desde la derecha / desde abajo; `center=True` alinea por el centro; - `frac=(0.5, 0.1)`: fracción de la pantalla; - `world=(x, y)`: coordenadas del mundo; - `unit=unidad`: sigue a una unidad. El texto y las barras sobre el mundo o sobre una unidad se anclan por el punto medio del borde inferior; `lift` los sube. Por defecto, los elementos del mundo y de unidades evitan la consola de abajo y el orbe de día y noche de arriba (`over_ui=True` los dibuja por encima). Los **colores** se pueden escribir como `(r, g, b)`, `(r, g, b, a)`, `"#RRGGBB"` o `"#RRGGBBAA"`. | Método | Qué dibuja | Parámetros habituales | |---|---|---| | `text(key, texto, ...)` | Cuadro de texto; `\n` para varias líneas | `color`, `bg` color de fondo (transparente si no se indica), `border`, `size`, `bold`, `shadow`, `width` (ajusta las líneas a este ancho), `radius` esquinas redondeadas | | `panel(key, título, [líneas...], ...)` | Panel (fondo oscuro semitransparente, borde dorado) | Igual que `text` | | `bar(key, 0..1, ...)` | Barra de progreso: vida, enfriamiento, barra de carga | `width`, `height`, `color`, `bg`, `border`, `text` | | `image(key, ruta, ...)` | Imagen local (png / jpg / bmp / gif) | `width`, `height` (tamaño original si no se indican) | | `circle(key, unidad_o_punto, radio, ...)` | Círculo en el suelo, pegado al terreno | `color` color de línea, `fill` relleno (con transparencia), `width` grosor de línea | | `path(key, [puntos...], ...)` | Línea quebrada en el suelo | `color`, `width`, `arrow` flecha al final; los puntos pueden ser coordenadas o unidades | ## HTTP (cualquier lenguaje) Backend de Farsight (solo escucha en local): ```http POST /api/instances/20/canvas {"set": [ {"key": "banner", "kind": "text", "text": "Lienzo desde 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 qué elementos hay dibujados ahora + cuántos fotogramas se han dibujado ``` `kind` es el nombre del método de Python y los parámetros se llaman igual; para una unidad se escribe su dirección `addr` de la instantánea. ## Escribir directamente en memoria compartida También se puede sin pasar por Python ni por Farsight: primero se envía una vez el comando semántico `canvas_enable` (código de operación 73 de W3P) y el runtime crea el bloque de memoria compartida `Local\War3Canvas_`: cabecera de 64 bytes + 256 entradas × 112 bytes + un pool de 64 KB para texto / puntos. Se escribe con seqlock (el número de secuencia pasa a impar → se escriben las entradas y el pool → el número de secuencia pasa a par); el runtime lo lee una vez por fotograma, si encuentra una escritura a medias sigue usando el fotograma anterior, y devuelve el número de fotogramas dibujados, el número de elementos y el contador de fallos. La implementación de referencia en Python es `sdk/python/w3canvas.py` y las estructuras están definidas en el header del protocolo; ver [Protocolo W3P](https://war3ai.com/es/docs/protocol/). ## Varios programas dibujando a la vez Los mods, Farsight, MCP y la pasarela pueden dibujar a la vez en la misma partida, pero solo hay un lienzo. La regla es: **cada programa solo toca sus propios elementos**. - Antes de escribir se toma un bloqueo con nombre, se leen los elementos que ya hay, se dejan los de los demás, se ponen los propios y se vuelve a escribir; - Cada elemento guarda quién lo dibujó (ID de proceso + número de secuencia dentro del proceso); si el programa que lo dibujó termina, se limpia la próxima vez que alguien escriba, y sus botones dejan de interceptar clics; - Los números de elemento salen de un contador compartido, así que no chocan. El SDK de Python ya lo hace así, y `clear()` solo borra lo propio. Si escribes tú directamente en la memoria compartida, sigue estas reglas; si no, borrarás lo que han dibujado otros. Los detalles de la estructura están en [Protocolo W3P](https://war3ai.com/es/docs/protocol/). ## Medidas y precauciones - Medido el 25 de septiembre de 2026 (1920×1080, velocidad 2×): 9 elementos, 0.27 ~ 0.34 ms por fotograma, unos 63 fotogramas / s, 0 fallos; escribir 9 entradas lleva 6 ms; cuando el héroe se mueve, los círculos, el texto y las barras de vida que lo siguen van a la par. La textura solo se vuelve a generar si cambia el contenido; si solo cambia la posición, no. - Se dibuja después de la interfaz del juego y antes del puntero del ratón: cubre las barras de vida, las unidades y la interfaz del propio juego, y el puntero queda por encima. Evita la consola de abajo y el orbe de día y noche de arriba, pero **no evita los paneles del propio mapa** (la tabla de clasificación y la cuenta atrás de la esquina superior derecha): no pongas tus paneles arriba a la derecha. - Fuera de una partida (menú principal, pantalla de resultados), los elementos colocados en coordenadas del mundo o sobre unidades no se dibujan; los que están en una posición de pantalla sí. - Los círculos en el suelo proyectan sobre el terreno cada uno de los 64 puntos de la circunferencia, así que cuando el terreno tiene desniveles la forma sube y baja con él; es lo correcto: se dibuja sobre el suelo real. - La primera vez hay que instalar el hook y precalentar las fuentes, lo que lleva alrededor de 1 segundo; mientras tanto no se dibujan los elementos de texto, pero los círculos y las líneas sí. - Si se produce un fallo durante el dibujo, deja de dibujar durante el resto de la sesión (la misma protección que las burbujas); `faults` en `stats()` pasa a 1. - Texto, rutas de imágenes y puntos comparten 64 KB, con un máximo de 256 elementos; las rutas de imagen deben ser rutas locales que el proceso del juego pueda leer. El panel de estado del [compañero IA](https://war3ai.com/es/docs/companion/) está dibujado con el lienzo: barra de vida, qué está haciendo, ánimo, bajas y curas. --- # Interfaz y entrada > Los botones y las tarjetas de elección del lienzo se pueden pulsar y se resaltan solos al pasar el ratón; registra atajos de teclado, haz clic en el suelo para elegir una posición, lee a qué apunta el ratón y qué tiene seleccionado el jugador local. Clics, atajos, habilidades lanzadas, el texto completo del chat y los jugadores que se van: todo entra en el flujo de eventos. Lo que dibuja el [lienzo](https://war3ai.com/es/docs/canvas/) ahora **se puede pulsar**. El runtime captura la entrada de la ventana del juego, y un programa externo puede: | Capacidad | En una frase | ¿Lo recibe el juego? | |---|---|---| | **Elementos clicables del lienzo** | Botones, tarjetas de elección, paneles: al hacer clic se emite `ui.click`, y se resaltan solos al pasar el ratón | El clic sobre el botón **no le llega** | | **Atajos de teclado** | Registra combinaciones como `F5` o `ctrl+shift+Q`; al pulsarlas se emite `hotkey` | Opcionalmente se tragan (junto con el carácter que generan) | | **Clic en el suelo** | Un clic sobre el mundo emite `mouse.world`, con las coordenadas del suelo | Opcionalmente se traga («haz clic en un punto para poner una torre») | | **Posición del ratón** | Se actualiza en cada fotograma: píxeles de pantalla, punto del suelo al que apunta, elemento del lienzo bajo el cursor | — | | **Selección** | A quién tiene seleccionado el jugador local; en cuanto cambia se emite `selection.changed` | — | Todo es **entrada local + dibujo local**: no entra en el flujo de órdenes, así que también es seguro en multijugador. Pero si en un callback cambias el mundo (crear unidades, cambiar atributos), eso sigue funcionando solo en partidas de un jugador. ## Python: g.ui ```python ui = g.ui # la primera vez, el runtime captura la entrada de la ventana ui.button("shop", "Comprar una poción (50 de oro)", screen=(40, 300), on_click=lambda g, ev: buy(g)) c = ui.choice("¡Has subido de nivel! Elige una recompensa", [("Fuerza +5", "Aguanta más"), ("Vel. de ataque +20%", "Pega más"), ("Invocar lobo", "Un ayudante más")], pause=True, on_pick=lambda g, i: give(g, i)) # una fila de tarjetas en el centro de la pantalla; pause=True pausa el juego mientras eliges i = c.wait(timeout=30) # también puedes esperar bloqueando (mientras tanto se siguen procesando los eventos, no se pierde ninguno) ui.hotkey("F5", lambda g, ev: g.say(hero, "¡Recibido!")) # por defecto se traga ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False) ui.mouse(on_click, capture=True, buttons=("left", "right")) # captura los clics en el suelo: avisa de izquierdo y derecho, y se los traga xy = ui.pick_point("Clic en el suelo: ¿dónde va la torre?") # versión bloqueante: el siguiente clic izquierdo en el suelo -> (x, y); Esc o timeout -> None ui.cursor() # {'screen': (x, y), 'world': (x, y, z) o None, 'hover': 'shop'} ui.toast("¡Llega la oleada 3!", seconds=3) ui.close() # retira tus controles y atajos; solo devuelve la entrada de la ventana si ningún otro programa la está usando g.close() # o desconecta del todo (también se puede escribir with Game(...) as g:) ``` Los callbacks reciben `(g, ev)` y se disparan cuando llamas a `g.events()`; los ejecutores de Bots y de [mods de juego](https://war3ai.com/es/docs/mods/) lo llaman en cada tick. Los clics sin callback van a `ui.clicks`. Si un callback lanza una excepción, solo se registra en el log; no afecta a los demás callbacks ni a los eventos. También puedes usar directamente la capa de abajo, el lienzo: `g.canvas.text(..., clickable=True, hover=color)`; los clics se reciben del flujo de eventos y `ev.key` es la key que diste al dibujar. Dibujar un elemento clicable activa la entrada automáticamente; no hace falta tocar antes `g.ui`. **Cómo se escriben los atajos**: `F1` ~ `F24`, `A` ~ `Z`, `0` ~ `9`, `numpad0` ~ `numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`, con `ctrl+`, `shift+` o `alt+` delante. > **Atención** > > Las letras y los números sin tecla modificadora chocan con la escritura en el chat y con los atajos del propio juego. Usa mejor teclas que el juego no ocupa, como F5 ~ F8, o combinaciones. ## Eventos nuevos `g.events()` ahora también entrega estos (los campos completos están en [Protocolo W3P](https://war3ai.com/es/docs/protocol/)): | kind | Cuándo | Campos de conveniencia | |---|---|---| | `ui.click` | Se hace clic en un elemento interactivo del lienzo | `.key` key del lienzo, `.button` (`'left'` / `'right'`), `.mods` teclas modificadoras | | `ui.hover` | El ratón entra en un elemento del lienzo o sale de él | `.key` (`None` al salir) | | `hotkey` | Se pulsa un atajo registrado | `.key` el atajo tal como se escribió, `.mods` | | `mouse.world` | Con los clics en el suelo activados, un clic sobre el mundo | `.x .y` coordenadas del suelo, `.button`, `.value` (1 = se lo tragó) | | `selection.changed` | Cambia la selección del jugador local | Obtén las unidades con `g.selection()` | | `spell.cast` | Una unidad lanza una habilidad (la habilidad entra en enfriamiento) | `.spell` código de cuatro caracteres, `.b` nivel, `.value` segundos de enfriamiento, `.x .y` punto de lanzamiento | | `message` | Aparece una línea en un marco de mensajes de la pantalla | `.text` texto completo, `.frame` qué marco, `.chat` (si es chat) | | `player.left` | Un jugador se va o es eliminado por derrota | `.player` | | `game.ended` | Se sale de la partida | — | ## Chat y mensajes en pantalla Lo que el jugador escribe en el chat se lee directamente de `.chat` en el evento `message`: ```python for ev in g.events(): if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow": ... # ev.chat = {'channel': 'Todos', 'sender': 'nombre del jugador', 'text': '-follow'} ``` `g.messages()` tiene además su propio cursor independiente, y ahí también están los avisos del juego («Necesitas más granjas», «No se puede construir ahí»). Al escribir un Bot, con él sabes por qué un comando no se llevó a cabo. ## Desde otros lenguajes - **Pasarela**: los métodos `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse` y `ui.cursor` están disponibles con el mismo nombre en la [pasarela](https://war3ai.com/es/docs/gateway/). Un cliente remoto no puede aportar funciones de callback, así que los clics y los atajos se reciben en los eventos enviados (el evento `ui.click` lleva la `key`). - **Escribir directamente en memoria compartida**: primero envía el comando semántico `input_enable` (código de operación 74 de W3P) y el runtime empieza a capturar la entrada; en el bloque de entrada `Local\War3Input_` tú escribes la tabla de atajos y los interruptores del ratón, y él escribe la posición del ratón, el punto del suelo al que apunta y el elemento bajo el cursor. El flag `0x40` de un elemento del lienzo significa «interactivo». La disposición está en [Protocolo W3P](https://war3ai.com/es/docs/protocol/). ## Varios programas a la vez Los mods, Farsight, MCP y cada sesión de la pasarela pueden poner botones y registrar atajos a la vez en la misma partida sin molestarse: - Cada programa registra sus propios atajos y su interruptor de clics en el suelo, y el SDK junta los de todos en una sola tabla para el runtime. Cada tecla aparece una sola vez, los eventos llegan a todos y cada uno reconoce sus atajos por la tecla; - `ui.close()` solo retira lo propio, y la entrada de la ventana solo se devuelve cuando se va el último programa; - Si un programa se cierra a la fuerza sin poder recoger: el runtime comprueba cada 2 segundos y, cuando todos los programas registrados han terminado, borra los atajos y la intercepción de clics en el suelo que dejaron, y los botones que dibujaron dejan de interceptar clics. ## Medido 2026-09-25, verificación en vivo en la instancia de prueba, 16/16: - Clic en un botón → `ui.click` + callback, y el contador de intercepciones del runtime sube en 1 (el juego no recibió ese clic); un clic fuera del botón no lo dispara; - F6 → `hotkey`; clic en el suelo → `mouse.world` (tragado); - Crear un paladín y seleccionarlo → `selection.changed`, y `g.selection()` coincide; lanzar Escudo divino → `spell.cast('AHds', 1, 35.0)`; - Texto del mapa → `message`; chat → `message`, y `.chat` separa quién habla y qué dice; - Declarar derrotado al ordenador → `player.left`; terminar la partida → `game.ended`. También se comprobaron uno a uno, con el ratón de una persona real, los clics en los botones y el resaltado al pasar por encima. ## Límites y precauciones - **La posición es la del ratón real**: el juego lee la posición del cursor del sistema, así que el hover y `cursor()` reflejan el ratón real. La intercepción solo actúa sobre las pulsaciones. - **Se dibuja debajo del puntero del ratón**: Warcraft dibuja el puntero como parte de la imagen en cada fotograma. El lienzo y las burbujas de diálogo se dibujan justo antes del paso en que el juego dibuja el puntero: cubren la interfaz del juego y el puntero queda por encima. Solo si en ese fotograma no se dibuja el puntero (está oculto o hay una cinemática) se vuelve a dibujar en el último paso. - **Escalado del sistema**: si escribes tus propias pruebas y envías clics con mensajes de ventana, las coordenadas que envía un proceso que no declara reconocimiento de DPI las amplía el sistema (medido: ×1.5 con un escalado del 150%). Declara primero el programa de pruebas como compatible con DPI. Los clics de una persona real no se ven afectados. - **La primera vez hay que precalentar las fuentes**, alrededor de 1 segundo. Durante ese tiempo los botones aún no se han dibujado y no se pueden pulsar. - **Fuera de una partida no se avisan los clics en el suelo**: en el menú principal y en la pantalla de resultados, `mouse.world` ni se emite ni se traga. - Si un clic se tragó al pulsar y, antes de soltar, cambias a otro programa o arrastras el ratón fuera de la ventana, el estado también se reinicia: no se traga además la siguiente vez que sueltes. - La 1.27 no tiene funciones para crear marcos de interfaz del juego (llegaron en la 1.31): aquí los botones y las tarjetas los dibuja el runtime, con el estilo que quieras, pero no aparecen en la jerarquía de menús del propio juego. --- # Canal JASS > Las 1291 funciones JASS que tienen los autores de mapas ahora se pueden llamar por su nombre desde fuera del juego: crear unidades, cambiar atributos, efectos, paneles, cuadros de diálogo, sonido, cámara, niebla… Cuatro formas de uso: consola Farsight, línea de comandos, HTTP y Python. Las **1291 natives de JASS** que los autores de mapas pueden usar en el script del mapa ahora se pueden llamar todas por su nombre desde fuera del juego: crear unidades, cambiar atributos, dibujar efectos, abrir paneles y cuadros de diálogo, reproducir sonidos, mover la cámara, cambiar la niebla… Sirven para personalizar el juego un paso más: ayudas para RPG, el [compañero IA](https://war3ai.com/es/docs/companion/), minijuegos propios, herramientas de depuración. | Forma | Ideal para | Por dónde se entra | |---|---|---| | **Página "Consola JASS" de Farsight** | Probar a mano, ajustando mientras miras | Barra lateral izquierda "Sistema → Consola JASS": escribe un script y pulsa ejecutar; a la derecha buscas funciones por categoría y con un clic las insertas en el script | | **Línea de comandos** | Probar a mano, o guardar scripts en archivos y ejecutarlos una y otra vez | `python -m openwar3 jass --inst 20` (interactivo), `-e "código"`, `my_script.j`, `--list palabra_clave` | | **HTTP** | Programas externos en cualquier lenguaje | `POST /api/instances/{n}/jass`, etc. (ver más abajo); el backend de Farsight solo escucha en local | | **Python** | Escribir esquemas, compañeros o herramientas | `g.jass.CualquierNative(...)`; los efectos visuales y las interacciones habituales están empaquetados en `openwar3.visual` | > **Atención** > > Tres límites, todos impuestos por el propio mecanismo: > > - Solo las **partidas de un jugador** (contra el ordenador, en tu equipo) pueden cambiar el mundo. Crear objetos o modificar unidades de forma unilateral desde tu equipo desincronizaría a los demás jugadores en multijugador, así que ahí solo se permiten funciones de solo lectura (`Get*`, `Is*`, `Count*`…). > - Solo para las herramientas de tu propio equipo; las llamadas desde una conexión como jugador (`Game(player=N)`) o en modo justo se rechazan. > - Solo para partidas sin conexión o en LAN creadas por ti. > > Para añadir cosas a la imagen en multijugador, usa el [lienzo](https://war3ai.com/es/docs/canvas/): lo dibuja el propio runtime y no cambia el estado del juego. ## Cómo escribir scripts La consola, la línea de comandos y HTTP usan el mismo lenguaje de script. Una sentencia por línea; **puedes pegar JASS tal cual** (`call` / `set` / `local`, `true` / `false` / `null`, códigos de cuatro caracteres como `'Hpal'`, comentarios `//`), o escribirlo al estilo de Python: ```text set h = hero() // integrada: nuestro héroe principal local texttag t = CreateTextTag() call SetTextTagText(t, "|cffffcc00+128 ¡Crítico!|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("Creada", u, "nivel del héroe", GetHeroLevel(h)) ``` - **Las variables se recuerdan**: en la misma instancia y la misma partida, las variables que un fragmento asigna con `set` se pueden usar en el siguiente; se borran solas al cambiar de partida, y también se pueden borrar a mano. - **Funciones integradas**: `hero()` nuestro héroe principal, `me()` el jugador local, `unit('hfoo')` busca una unidad, `unit_at(x, y)`, `wait(segundos)`, `print(...)`. De una unidad se pueden leer `.x`, `.y`, `.hp`, `.hp_max`, `.mana`, `.type`, `.owner` y `.level`; se admiten las cuatro operaciones aritméticas y las comparaciones. - **No se admiten** `if`, `loop` ni `function`: para escribir lógica, usa `g.jass` desde Python (son llamadas a funciones normales) o conviértelo en un [esquema](https://war3ai.com/es/docs/schemes/). - Si hay un error, te dice en qué línea y por qué (no existe esa función, número de parámetros incorrecto, variable no definida…); las sentencias anteriores al error ya se han aplicado. Parámetros y valores de retorno: | En la firma | Qué pasar | Notas | |---|---|---| | Entero | Un número; los códigos de cuatro caracteres como `'Hpal'` se convierten solos | | | Real | Un número | El runtime lo convierte al formato que espera el motor | | Booleano | `true` / `false` | | | Cadena | `"..."` | Admite chino y los códigos de color del juego; las que el juego guarda (texto flotante, paneles, botones, comandos de chat) se copian en el momento, así que es seguro | | Handle | Un handle guardado en una variable, o una unidad (cosas como `hero()` se convierten solas en handle) | | | Función (code) | Solo `null` | Desde fuera no se puede pasar una función JASS; algo como `TimerStart(t, 60, false, null)` sí funciona | | Devuelve cadena | — | El motor devuelve un índice de la tabla de cadenas y el texto no se puede leer. Para nombres de unidades, usa `g.map_data.name_of` | ## Categorías Las funciones están clasificadas por su nombre; la parte derecha de la consola y `--list` usan esta clasificación: | Categoría | Cantidad | Ejemplos | |---|---|---| | Efectos visuales | 80 | Texto flotante, rayos entre unidades, efectos, imágenes en el suelo, marcas en el suelo, color / escala / animaciones de unidades | | Paneles de interfaz | 146 | Paneles multilínea, tabla de clasificación, ventana de cuenta atrás, cuadros de diálogo, misiones, texto en pantalla, avisos en el minimapa, diálogo con retrato, filtros de pantalla completa | | Cámara | 44 | Campos de cámara, desplazamiento, temblor de cámara | | Sonido y música | 50 | Crear y reproducir sonidos, poner música | | Niebla y visión | 25 | Zonas visibles, activar / desactivar la niebla | | Objetos / héroes / unidades | 63 / 32 / 161 | Crear objetos, fijar el nivel de un héroe, cambiar de dueño, añadir habilidades | | Jugadores / alianzas / recursos | 71 | Fijar alianzas, cambiar el oro y la madera | | Disparadores / eventos / temporizadores | 62 | Crear disparadores, registrar eventos, temporizadores | | Terreno / clima / destructibles | 45 | Efectos de clima, modificar el terreno, crear destructibles | | Flujo de la partida | 57 | Velocidad de juego, pausa, hora del día | | Otros | … | Grupos de unidades y regiones, almacenamiento, scripts de IA del ordenador, conversión de tipos y matemáticas, respuestas a eventos… | El 24 de septiembre de 2026 se llamaron una a una en partida real, comprobando el efecto a simple vista, **94**; el resto siguen exactamente el mismo camino, solo que no se ha revisado su efecto una por una. > **Nota** > > Las funciones de "respuesta a eventos" (`GetTriggerUnit`, `GetClickedButton`…) solo tienen valor en el instante en que se ejecuta el disparador; llamadas desde fuera devuelven 0 o vacío. Para saber "si ha ocurrido algo", usa los contadores de eventos que se explican más abajo. ## HTTP Backend de Farsight (por defecto `127.0.0.1:8866`, solo escucha en local): ```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": {...}} -> si hay error: {"ok": false, "error": "第 2 行:...", "line": 2} (error = "línea 2: ...") POST /api/instances/20/jass/call {"name": "SetUnitScale", "args": [{"unit": 599669636}, 1.4, 1.4, 1.4]} POST /api/instances/20/jass/reset borra las variables recordadas ``` Los parámetros de unidad se escriben `{"unit": dirección}`, donde la dirección es el `addr` de la unidad en la instantánea. Medido: 60 ~ 90 ms por petición. ## Python: g.jass y openwar3.visual ```python j = g.jass t = j.CreateTextTag() j.SetTextTagText(t, "Hola", 0.024) # mismas reglas de parámetros que en los scripts; las unidades y objetos de la instantánea se pasan tal cual j.signature("CreateImage") # consultar la firma ``` `openwar3.visual.Visual(g)` empaqueta los efectos visuales habituales ya probados, una línea para cada uno (llama a `v.tick()` en cada tick: borra lo que ha caducado y mueve las líneas y los círculos que siguen a unidades; `v.clear()` lo borra todo): | Método | Efecto | |---|---| | `float_text(texto, unidad_o_punto, ...)` | Texto flotante: números de daño, avisos sobre la cabeza; admite chino y colores | | `link(a, b, kind)` | Una línea entre dos unidades que las sigue: correa / vínculo espiritual / drenaje de vida / ola de curación | | `effect(modelo, unidad_o_punto, ...)` | Modelo de efecto: sobre la cabeza, a los pies, o de una sola vez (explosión, columna de luz) | | `ring(unidad_o_punto, radio, color)` | Círculo de alcance en el suelo: alcance de una habilidad, zona peligrosa, punto de reunión; puede seguir a una unidad | | `ping(punto, color)` | Aviso en el minimapa | | `board(título, líneas...)` | Panel multilínea arriba a la derecha (con iconos); se puede modificar celda a celda | | `countdown(título, segundos)` | Ventana de cuenta atrás arriba a la derecha; el propio juego lleva la cuenta | | `scene(nombre, frase, portrait)` | Diálogo con retrato: el retrato de abajo cambia a una unidad que habla y aparece en pantalla el subtítulo "nombre: frase" | | `screen_tint(color, alpha)` | Filtro de pantalla completa (por defecto, bordes rojizos: aviso de poca vida) | | `sound(ruta)` / `reveal(punto, radio, segundos)` / `look(unidad, ...)` | Reproducir un sonido / despejar la niebla de una zona / cambiar el color de una unidad, agrandarla, reproducir una animación, hacerla destellar | ## Interacción: saber qué hace el jugador sin escribir funciones JASS En JASS, para responder al jugador hay que escribir funciones de disparador, y desde fuera no se pueden pasar funciones. La solución: **crear un disparador vacío, sin condiciones ni acciones, registrar solo el evento y contar cuántas veces se ejecuta.** Medido: un disparador vacío cuenta igual. | Método | Uso | |---|---| | `chat_commands(["-follow", "-stay"])` → `.poll()` | Comandos que el jugador escribe en el chat (coincidencia exacta o por prefijo) | | `menu(título, [botones...])` → `.clicked()` | Menú de botones en el centro de la pantalla: cuál se ha pulsado | | `hotkeys(("left", "right", "up", "down", "esc"))` → `.poll()` | Cuántas veces se han pulsado las flechas y Esc | | `on("TriggerRegister...Event", args...)` → `.poll()` | Cuántas veces ha ocurrido cualquier evento JASS: muerte de una unidad, entrada en una región, daño recibido, temporizadores… | La limitación es que solo sabes "cuántas veces ha ocurrido", no "quién ha sido ni qué ha escrito". Para distinguir quién, crea un contador para cada objeto. Así es como se conectan los comandos de chat del [compañero IA](https://war3ai.com/es/docs/companion/). ## Precauciones - **Lo que crees, bórralo tú**: texto flotante, líneas, imágenes, paneles, disparadores… si no los borras, se quedan ahí (`Visual.clear()` borra lo que ha creado él). El juego admite como mucho unos 100 textos flotantes a la vez. - **Las funciones BJ no son natives**: las del tipo `CreateTextTagUnitBJ` están montadas con natives dentro del script del mapa y aquí no existen; mira su implementación y llama directamente a las natives. - **Algunas constantes hay que convertirlas antes**: por ejemplo, `ConvertPlayerColor(1)`, `ConvertFogState(4)` (los valores están en common.j). - Una llamada tarda unos 13 ms (incluida la conversión de handles); en la capa del protocolo son los códigos de operación 70 ~ 72 de W3P; ver [Protocolo W3P](https://war3ai.com/es/docs/protocol/). --- # Mods de juego > Un esquema no tiene por qué ser una IA que juega por ti: también puede ser un conjunto de reglas. Juegas tú en la ventana del juego y el mod prepara el inicio, genera enemigos, da recompensas, te pone botones y tarjetas de elección en pantalla y decide el resultado. Hereda de openwar3.Mod: un archivo, un modo de juego. Los [esquemas de IA](https://war3ai.com/es/docs/schemes/) son de dos tipos: `kind: bot` es una IA que juega por ti; `kind: mod` es **un conjunto de reglas**: juegas tú en la ventana del juego y el mod plantea el reto: cómo se prepara el inicio, qué enemigos aparecen según el tiempo o los eventos, qué recompensas da, qué botones y tarjetas te pone en pantalla y cuándo se gana. Un mod solo usa capacidades que ya existen: [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/) (botones y tarjetas clicables, atajos de teclado, clics en el suelo), el [lienzo](https://war3ai.com/es/docs/canvas/) (paneles, barras de progreso, rutas), el [canal JASS](https://war3ai.com/es/docs/jass/) (crear unidades, cambiar atributos, dar objetos) y el flujo de eventos (muertes, subidas de nivel, habilidades lanzadas, chat). ## Dos ejemplos Se eligen en Farsight, en "Esquemas de IA" → "Integrados": | Mod | Cómo se juega | Capacidades que usa | |---|---|---| | **Roguelike de héroe** `builtin/hero-roguelike` | Solo tienes un paladín y los monstruos te rodean en oleadas desde todos los lados; cada vez que subes de nivel eliges una de tres mejoras en el centro de la pantalla (el juego se pausa mientras eliges); si aguantas 10 oleadas ganas, y si muere el héroe pierdes | `g.ui.choice` (tarjetas clicables + pausa), eventos `hero.levelup` / `killed` / `spell.cast`, `-help` en el chat, JASS para cambiar los atributos del héroe y dar objetos | | **Defensa sin fin** `builtin/endless-defense` | Los monstruos salen del punto de inicio contrario y cargan contra tu edificio principal siguiendo una línea roja en el suelo; cada oleada que resistes da oro; pulsa el botón en pantalla o F7 para llamar antes a la siguiente oleada, con recompensa ×1.5; pulsa F8 y haz clic izquierdo en el suelo para poner una torre de flechas gratis (clic derecho para cancelar) | `g.ui.button`, `g.ui.hotkey`, `g.ui.mouse` (captura de clics en el suelo), panel / barra de progreso / ruta del lienzo, JASS para generar monstruos y dar oro | Cada ejemplo tiene unas 150 líneas; el código está en `brains/examples/mod_hero_roguelike.py` y `brains/examples/mod_endless_defense.py`. ```bash python tools/play.py --bot brains/examples/mod_hero_roguelike.py --inst 9 # lanza una partida, el mod toma el control y tú juegas en la ventana del juego ``` ## Escribe un mod ```python from openwar3 import Mod class Survive(Mod): name = "survive" def on_start(self, g): super().on_start(g) # comprueba que es una partida de un jugador + neutraliza al rival del ordenador self.foe = self.wave_player(g) # un jugador de una ranura vacía hace de «jugador de oleadas»: sin alianzas con nadie y sin IA del ordenador self.every(30, self.wave) # una oleada cada 30 segundos de juego (se detiene durante la pausa) 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", "Han destruido el edificio principal") ``` `Mod` añade lo siguiente a `Bot`: | Método / atributo | Descripción | |---|---| | `on_start / on_tick / on_event / on_end` | Igual que en Bot; si sobrescribes `on_start` / `on_tick`, llama primero a `super()` | | `every(segundos, fn, first=)` / `after(segundos, fn)` | Temporizadores que avanzan con el **tiempo de juego**; el callback es `fn(g)` | | `finish(result, reason)` | Termina la partida (`'win'` / `'loss'` / `'unknown'`): el ejecutor se detiene en el siguiente tick, dibuja un panel con el resultado en el centro de la pantalla y los resultados del esquema se registran según esto | | `wave_player(g)` | El primer jugador de ranura vacía, para usarlo como jugador de oleadas | | `spawn_ring(g, jugador, unidad, cantidad, centro, radio, attack_to=)` | Crea unidades en un anillo; aunque una oleada tenga decenas, no se atasca; devuelve handles de JASS | | `alive_of(g, jugador)` / `attack_move_all(g, jugador, punto)` | Las unidades vivas de un jugador / todas atacan-mueven hacia ese punto (llámalo cada pocos segundos y los monstruos te seguirán) | | `home(g)` / `hud(g, título, líneas)` | Posición de nuestro edificio principal / panel de información arriba a la derecha | | `neutralize_ai = True` | Neutraliza al rival del ordenador al empezar: sus unidades se pausan cada 5 segundos y su oro y su madera se ponen a cero. Los mapas de enfrentamiento siempre tienen un ordenador, y cuando el mod pone sus propias reglas no conviene que moleste | | `single_player_only = True` | Se niega a ejecutarse si hay otros jugadores humanos (el JASS que cambia el mundo desincronizaría a los demás) | | `linger_s = 6` | Cuando hay resultado, cuántos segundos se queda en la pantalla del resultado antes de terminar | `finish()` también funciona en `Bot`: un Bot normal también puede declarar el final por su cuenta. ## Convertirlo en esquema y compartirlo En `scheme.json` pon `"kind": "mod"` y define una subclase de `Mod` en el archivo de entrada: ```json {"id": "survive", "name": "Resiste 10 oleadas", "kind": "mod", "entry": "survive.py", "class": "Survive"} ``` Un mod **nunca usa el modo justo** (es el árbitro que plantea el reto: necesita ver todo el mapa y cambiar el mundo) y **su resultado no se decide con las reglas de las partidas competitivas** (lo comunica `finish`); aunque el manifiesto incluya `fair` / `judge`, no tienen efecto. Exportar el zip, importar, confiar y los resultados funcionan exactamente igual que en los esquemas de Bot; ver [Esquemas de IA](https://war3ai.com/es/docs/schemes/). Un mod también es código: antes de ejecutar por primera vez el mod de otra persona, también hay que confirmar la confianza. ## Medido 2026-09-25, en la instancia de prueba: - **Roguelike de héroe**: sale la primera oleada y el panel de arriba a la derecha se va actualizando; al subir el héroe a nivel 3 → aparecen las tarjetas en el centro de la pantalla y el reloj de juego se detiene; dos clics en tarjetas → se aplican dos mejoras (fuerza 22 → 27) y el reloj vuelve a correr. - **Defensa sin fin**: el panel, la ruta en el suelo y el botón están ahí; F8 + clic en el suelo → aparece una torre defensiva junto al edificio principal; pulsar el botón antes de limpiar la oleada actual → aviso «esta oleada aún no está limpia». ## Límites - **Solo en partidas de un jugador**: crear unidades y cambiar atributos pasa por el canal JASS, que en multijugador desincroniza. Lo impone el modelo lockstep; los modos multijugador tendrán que esperar a un canal de sincronización (ver la [hoja de ruta](https://war3ai.com/es/roadmap/)). - El mod ve todo el mapa: es quien plantea el reto, no un jugador. - En los mapas de enfrentamiento, el rival del ordenador solo queda «neutralizado», no eliminado (eliminarlo activaría la condición de victoria de las reglas de enfrentamiento). --- # Consola Farsight > Consola web local y también el único punto de entrada: configura el directorio del juego, arranca y detén los servicios y las instancias del juego, configura la siguiente partida, mira qué piensa la IA, da órdenes a mano, dirección de cámara, historial de partidas. Farsight es la consola web que se ejecuta en tu equipo y **solo escucha en 127.0.0.1**. También es el único punto de entrada de todo el sistema: iniciar partidas, cambiar de IA, la pasarela, las burbujas de diálogo y el LLM local se manejan desde aquí, sin tener que buscar otros scripts. ```bash start.bat # comprueba el despliegue y abre Farsight en http://127.0.0.1:8866 start.bat 5 6 # además inicia la prueba en las instancias 5 y 6 (juego + cerebro de referencia) start.bat restart # solo reinicia el backend de Farsight (tras cambiar el código del servidor; el juego y los servicios no se ven afectados) stop.bat # lo detiene todo por completo ``` El puerto se cambia en `ports.console` de `openwar3.json` (8866 por defecto). ## Centro de control La página de inicio de Farsight. - **Directorio del juego**: búscalo automáticamente o elígelo tú; comprueba la versión del juego y extrae los datos de tu propio juego. - **Servicios locales**: [pasarela](https://war3ai.com/es/docs/gateway/), [burbujas de diálogo](https://war3ai.com/es/docs/speech/), LLM local (LM Studio) y vista previa del sitio web; cada tarjeta permite iniciar, detener, reiniciar y ver el log. Además muestra si algún cliente ha conectado el servidor [MCP](https://war3ai.com/es/docs/mcp/). - **Comprobación del entorno**: si está instalada cada pieza: Python, archivos del runtime, datos del juego, datos de AMAI, etc. - **Detener todo** (arriba a la derecha): detiene por orden las instancias del juego, la IA, la pasarela, las burbujas, el modelo local que usa este sistema y el backend de Farsight; equivale a hacer doble clic en `stop.bat`. El servidor MCP lo gestionan clientes como Claude, así que no se detiene; tampoco se cierra el propio programa LM Studio. ## Páginas | Grupo | Página | Qué hace | |---|---|---| | Principal | Centro de control | Ver la sección anterior | | Partida | Resumen | Visión general de la partida en la instancia actual | | | Mando en el campo | Vista del mapa; puedes dar órdenes a mano (las órdenes manuales tienen la prioridad más alta en la tabla de reclamación: 95) | | | Datos de unidades | Orden, objetivo de la tarea, maná, nivel y experiencia de héroe, enfriamientos de habilidades e inventario de cada unidad | | | Decisiones de IA / de combate | Qué piensa el cerebro de referencia en este tick y el detalle de cada decisión de combate | | | Asesor de estrategia | Estado del [LLM como asesor](https://war3ai.com/es/docs/llm-coach/): si el servicio del modelo está activo, si cada instancia está conectada, el último consejo y la entrada que vio | | | Dirección | Cámara automática, barras de vida sobre las unidades | | | Burbujas de diálogo | Hacer hablar a las unidades, conversar con el modelo local, tertulia de campesinos, diálogos de cámara, reacciones a la partida, ajustes del modelo. Ver [Burbujas y modelos locales](https://war3ai.com/es/docs/speech/) | | | Velocidad de órdenes | APM y caudal de comandos | | | Eventos y entrada | Qué ha pasado en la partida: habilidades lanzadas, chat y mensajes en pantalla, clics en botones, atajos, clics en el suelo, selección, jugadores que se van; con filtro por categoría. Al lado, la posición del ratón, el elemento bajo el cursor y la selección local. Ver [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/) | | Registros | Logs / historial | Fuentes de log de cada instancia; resultado, duración y pico de tropas de cada partida | | | Notas de problemas | Pulsa Pause/Break en el juego para pausarlo y anotar el momento; luego añade aquí la descripción | | Sistema | Instancias y partida | Arranca y detén instancias; configura el mapa de la **siguiente partida** (mapas de combate, y también mapas RPG / personalizados), las razas de ambos bandos, la dificultad y la velocidad; elige un esquema de IA para cada instancia; "Iniciar prueba" lanza juego + IA con un clic | | | Esquemas de IA | Importa, exporta, copia, marca como de confianza y borra esquemas, cambia el esquema de una instancia (incluso en la partida en curso: la nueva IA toma el relevo al instante) y consulta los resultados de cada esquema. Ver [Esquemas de IA](https://war3ai.com/es/docs/schemes/) | | | Consola JASS | Escribe un script JASS y pulsa Ejecutar; a la derecha, consulta las 1291 funciones por categoría y haz clic en una para insertarla en el script; las variables se conservan durante toda la partida. Ver [Canal JASS](https://war3ai.com/es/docs/jass/) | | | Conexión y extensión | Estado de la pasarela y arranque con un clic; direcciones de conexión generadas por rol (desarrollo / jugador / espectador), comandos y configuración para conectar el MCP; ejemplos en JS y Python. Ver [Pasarela](https://war3ai.com/es/docs/gateway/) y [MCP](https://war3ai.com/es/docs/mcp/) | | | Datos y disco | Cuánto disco ocupa cada tipo de dato de ejecución (grabaciones, historial de partidas, logs, etc.), cuánto se ha añadido en el último día y qué se puede borrar (Farsight nunca borra nada automáticamente) | | | Ajustes | Directorio del juego, idioma de la interfaz, apariencia (oscuro / claro, moderno / estilo Warcraft), etc. | | | Comentarios y sugerencias | Si encuentras un problema o tienes una sugerencia, envíanoslo directamente; la información de diagnóstico solo se adjunta si marcas la casilla, y puedes previsualizarla antes de enviarla | Pulsa Ctrl + K para abrir la paleta de comandos: cambiar de página, cambiar de instancia, terminar la partida actual, lanzar un cerebro nuevo. En la parte inferior de la barra lateral, "Novedades" muestra lo último que se ha añadido a Farsight y a la plataforma. Al arrancar (y después cada 6 horas), Farsight pregunta a War3AI.com si hay una versión nueva y, si la hay, te avisa. Cuando Farsight se actualiza, aparece un aviso en la parte superior de la página: guarda lo que estés escribiendo y pulsa "Recargar". ## Multiinstancia `runtime/farm.py` se encarga de abrir varias (Farsight lo llama por ti al arrancar y detener instancias): copia el lanzador original `War3.exe` y lo renombra a `War3-.exe` (sin modificar ningún archivo del juego); cada instancia tiene un número y un directorio (`bin/inst/`). Al terminar una partida, la siguiente arranca sola según `next_game.json` (es lo que editas en la página "Instancias y partida" de la consola). > **Consejo** > > Tu Bot se conecta a una instancia concreta con `--inst N`. En la página "Instancias y partida" de la consola ves qué números están en uso; no choques con el cerebro de referencia. ## Página para directos `http://127.0.0.1:8866/live` es una página de log con desplazamiento, pensada para usarla como "fuente de navegador" en OBS; muestra las decisiones de la IA y el estado de la partida. ## API El servidor de la consola es un conjunto de API REST + WebSocket locales (estado de las instancias, configuración de la siguiente partida, detalle de unidades, órdenes manuales, logs, historial de partidas, dirección de cámara, esquemas de IA, llamadas JASS, lienzo…); la web es solo uno de sus clientes y cualquier programa, en cualquier lenguaje, puede llamarlas directamente. La lista de endpoints está en la cabecera de `console/server/app.py`; el uso de los grupos de esquemas, JASS y lienzo se explica en [Esquemas de IA](https://war3ai.com/es/docs/schemes/), [Canal JASS](https://war3ai.com/es/docs/jass/) y [Lienzo](https://war3ai.com/es/docs/canvas/), respectivamente. --- # Esquemas de IA > Un esquema es una IA completa. Se cambia con un clic en Farsight, incluso en plena partida, y el nuevo toma el control al instante; exporta un zip para compartirlo, importa esquemas de otros para probarlos, y los resultados de cada esquema se registran solos. Un **esquema** = una IA completa: una carpeta + un manifiesto `scheme.json` + código. Cada instancia del juego elige un esquema; en Farsight se cambia con un clic, y **también en la partida en curso: el nuevo esquema toma el control al instante**. Los esquemas que comparten otros se importan en un **espacio aparte**, sin afectar a los tuyos; si quieres modificar uno, usa "Copiar a los míos". ```text schemes/ mine// mis esquemas: escritos por ti o copiados de otro para modificarlos (cámbialos a tu gusto; se aplican en la siguiente partida) installed// instalados: aquí se descomprimen los zip que comparten otros (antes de la primera ejecución hay que confirmar la confianza) brains/xwar3/ integrado: cerebro de referencia (IA completa) brains/examples/ integrados: cuatro ejemplos didácticos hello / rush / macro / micro, el ejemplo de compañero buddy y dos mods de juego (Roguelike de héroe, Defensa sin fin) ``` Un esquema no tiene por qué ser una IA que juega por ti: un esquema `kind: mod` es un **conjunto de reglas de juego**; juegas tú y él plantea el reto. Ver [Mods de juego](https://war3ai.com/es/docs/mods/). ## Usarlos en Farsight Página "Esquemas de IA" (barra lateral izquierda "Sistema → Esquemas de IA"): | Acción | Qué hace | |---|---| | Importar esquema (zip) | Lo instala en `installed/`; si ya hay uno instalado con el mismo id, pregunta si quieres sustituirlo (tras sustituirlo hay que volver a confirmar la confianza) | | Usar en instancia… | Elige la instancia + "Aplicar ahora" (detiene la IA actual y el nuevo esquema toma el control de esta partida) o "Aplicar en la próxima prueba" | | Copiar a los míos | Hace una copia en `mine/`, con autor "yo" y versión 0.1.0, y anota de qué esquema y de qué versión se copió | | Exportar zip | Lo empaqueta como `-.zip`; enviárselo a alguien es compartirlo | | Abrir carpeta | Abre el directorio del esquema en el Explorador de archivos para editar el código directamente | | Confiar | Obligatorio antes de ejecutar por primera vez un esquema de otra persona (ver "Confianza y seguridad" más abajo) | | Resultados recientes | Victoria o derrota, duración y motivo del final de cada partida de este esquema | | Eliminar | Solo se pueden eliminar los "míos" y los "instalados"; no se puede eliminar uno que esté usando alguna instancia | La tarjeta de cada instancia también tiene una fila nueva, "Esquema de IA": eliges el esquema en el desplegable → "Cambiar (aplicar ahora)". Si la instancia no está en marcha, el botón se llama "Seleccionar", y el siguiente "Iniciar prueba" arrancará la IA con ese esquema. ## El manifiesto scheme.json ```json { "format": 1, "id": "fast-rush", "name": "Rush de tres minutos", "version": "1.2.0", "author": "Fulano", "description": "Una frase que explica qué estrategia sigue esta IA", "entry": "rush_bot.py", "class": "RushBot", "fair": true, "hz": 5, "races": ["human", "orc"], "license": "MIT" } ``` | Campo | Obligatorio | Descripción | |---|---|---| | `id` | ✔ | Minúsculas, dígitos, `-` y `_`; de 2 a 41 caracteres | | `entry` | ✔ | Un archivo `.py` del directorio del esquema (no se admiten rutas absolutas ni `..`) | | `kind` | | Por defecto `bot` (subclase de `openwar3.Bot`, juega por ti); `mod` = [mod de juego](https://war3ai.com/es/docs/mods/) (subclase de `openwar3.Mod`; nunca usa el modo justo y el resultado no se decide con las reglas de las partidas competitivas) | | `class` | | Nombre de la subclase de Bot (o de Mod) en el archivo de entrada; si no se indica, se toma la última subclase de `openwar3.Bot` del archivo de entrada | | `fair` | | Por defecto `true`: solo ve lo que está en su visión, la misma regla que en la Arena. `false` = ve todo el mapa, y solo así puede usar el [canal JASS](https://war3ai.com/es/docs/jass/) (el compañero lo necesita) | | `judge` | | Por defecto `true`: el resultado se decide con las reglas de las partidas competitivas. Los esquemas de RPG / de compañero ponen `false` | | `hz` | | Cuántas veces por segundo se llama a `on_tick`; 5 por defecto | | `format` | | Versión del formato del manifiesto, actualmente 1; si es más nueva que el OpenWar3 instalado, se rechaza y se pide actualizar | | El resto | | `name`, `version`, `author`, `description`, `races`, `license`, `homepage` y `forked_from` solo se usan para mostrarlos | El directorio del esquema se añade a la ruta de búsqueda de módulos de Python, así que el archivo de entrada puede hacer `import` de otros archivos del mismo directorio. Los paquetes de terceros (numpy, torch…) no se instalan solos: indica claramente en `description` qué hace falta. **El esquema más pequeño cabe en dos archivos**: ```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": "Mi primera IA", "entry": "my_bot.py"} ``` Ponlo en `schemes/mine/my-first/`, refresca Farsight y ya aparece. Un punto de partida aún más cómodo: elige un ejemplo en "Integrados" y pulsa "Copiar a los míos". ## Ejecución y resultados Los esquemas los ejecuta el **ejecutor de esquemas** (es lo que arrancan "Iniciar prueba / Cambiar" en Farsight): ```bash python tools/run_scheme.py --inst 20 --scheme builtin/micro --hours 6 ``` - Cada instancia tiene un proceso supervisor permanente, y **cada partida arranca un subproceso** que ejecuta el esquema: si el código del esquema falla, no arrastra al supervisor; si cambias el código de uno de "mis esquemas", la siguiente partida ya usa el nuevo. - Al terminar cada partida se anota una línea de resultados: esquema, versión, autor, victoria o derrota, motivo, duración de la partida y número de errores. El porcentaje de victorias de Farsight se calcula a partir de aquí. Cómo se decide el resultado: | Situación | Se registra como | |---|---| | El rival se queda sin edificios | Victoria | | Nos quedamos sin edificios (aunque queden tropas vivas: así se pierde en una partida competitiva) | Derrota | | Nos quedamos sin unidades | Derrota | | Se termina / detiene a mano desde Farsight | Sin decidir | | Al cambiar de esquema, la partida ya llevaba más de 60 segundos de juego (relevo a mitad de partida) | Se cuenta aparte, **no entra en el porcentaje de victorias** | | El reloj de juego lleva mucho tiempo parado | Sin decidir | Cuando hay resultado, el ejecutor cierra la pantalla de resultados, abre la siguiente partida según "Siguiente partida" y el esquema vuelve a tomar el control, así que puedes dejarlo toda la noche acumulando resultados. La pausa no cuenta como final: durante la pausa el Bot sigue funcionando y solo se para el reloj de juego. ## Confianza y seguridad **Un esquema es código y se ejecuta con los mismos permisos que tú** (puede leer y escribir archivos, puede conectarse a la red). Por eso: - Los esquemas de `installed/` **no son de confianza** por defecto: Farsight y el ejecutor se niegan a ejecutarlos hasta que pulses "Confiar"; - Sustituir la instalación de un esquema con el mismo id **restablece la confianza** (una versión nueva es código nuevo); - Al importar se comprueba que el zip no pase de 50 MB ni de 2000 archivos; no se admiten rutas absolutas ni `..` (para impedir escribir fuera del directorio del esquema); si el manifiesto no es válido o el archivo de entrada no existe, se rechaza directamente. > **Atención** > > Antes de confiar, pulsa "Abrir carpeta" y lee el código. Acepta esquemas solo de gente de la que te fíes. ## API (para scripts) | Endpoint | Descripción | |---|---| | `GET /api/schemes` | Lista de esquemas + resultados + el esquema elegido y el que se está ejecutando en cada instancia | | `GET /api/schemes/results?ref=` | Las últimas 30 partidas de un esquema | | `POST /api/schemes/import` | Importar un zip | | `GET /api/schemes/export?ref=` | Descargar el zip | | `POST /api/schemes/fork` | Copiar a los míos | | `POST /api/schemes/trust` | Confiar | | `DELETE /api/schemes?ref=` | Eliminar (se rechaza si alguna instancia lo está usando) | | `POST /api/instances/{n}/scheme` | Cambiar el esquema de una instancia: toma el control de esta partida al instante, o se aplica en la próxima prueba | En Python se usa directamente la biblioteca: `from openwar3 import schemes` (`list_schemes`, `install_zip`, `export_zip`, `fork`, `trust`, `stats`…). ## Más adelante: web de esquemas El zip exportado es la unidad que se comparte, así que la web solo tiene que añadir una capa por fuera: subir con un clic desde Farsight; descargar desde la web, con exactamente las mismas comprobaciones que "Importar esquema" y la misma confirmación de confianza; y, si quieres, enviar los resultados para que la web agregue el porcentaje de victorias por versión. En Farsight ya está reservado el sitio del botón "Compartir en la web de esquemas". El avance está en la [hoja de ruta](https://war3ai.com/es/roadmap/). --- # Burbujas y modelos locales > Haz que cualquier unidad del juego muestre burbujas de diálogo con cualquier identidad; conecta un LLM local y cada frase que entra recibe una respuesta sobre la cabeza de la unidad. Las burbujas son una capa para el espectador: no afectan al resultado y sirven para directos, comentarios y depuración. - Cualquier unidad puede hablar con cualquier identidad; varias unidades pueden hablar a la vez; - El tamaño de letra, el color, el ancho, la cola, la opacidad y la velocidad de escritura de cada burbuja se pueden personalizar por separado; - Se conecta directamente a un LLM local (LM Studio) con salida en streaming: la burbuja se actualiza mientras se genera el texto. ## Desde un Bot Lo más sencillo es el `say` que incluye el SDK: ```python g.say(hero, "¡Conmigo, a la carga!", seconds=4) ``` ## Arranque e interfaz **Lo más cómodo: el "Centro de control" de la página de inicio de Farsight**. Primero pulsa "LLM local → Iniciar y cargar el modelo" (el servicio local de LM Studio + cargar en la VRAM el modelo configurado) y después "Burbujas de diálogo → Iniciar". En las tarjetas puedes ver el log, detener y reiniciar. La interfaz está en la página "Burbujas de diálogo" de la barra lateral izquierda de Farsight: hacer hablar a unidades (elegir la unidad, escribir el texto, ajustar el estilo, conversar con el modelo), tertulia de campesinos, diálogos de cámara, reacciones a la partida y ajustes del modelo; todo actúa sobre la instancia elegida en la barra superior. También desde la línea de comandos: ```bash python speech/speak_launch.py # arranca el servicio del modelo local + carga y precalienta el modelo + arranca la API de burbujas python speech/speak_launch.py --restart # reinicia la API tras cambiar el código python speech/speak_launch.py --stop # detiene la API y descarga el modelo de la VRAM ``` Cada paso es "si ya está, se omite", así que ejecutarlo varias veces no tiene efectos secundarios. ## API HTTP Por defecto, `http://127.0.0.1:8872/` (el puerto está en `ports.speech` de `openwar3.json`); cualquier programa puede llamarla. ### Hacer hablar a una unidad `POST /api/say` ```json { "inst": 16, "bubbles": [ { "unit": "0x14A12614", "name": "Rey de la Montaña", "text": "¡Conmigo, a la carga!" }, { "unit": "0x14A12924", "name": "Archimago", "text": "Yo lanzo la Ventisca.", "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } }, { "screen": [960, 110], "key": 1, "name": "Narrador", "text": "La primera oleada de orcos llega en 30 segundos.", "style": { "tail": false, "type_ms": 0 } }, { "world": [-4684, 2644], "key": 2, "text": "Punto de reunión", "style": { "font_px": 16 } } ] } ``` | Campo | Descripción | |---|---| | `unit` / `world` / `screen` | Uno de los tres: sigue a una unidad (si tiene barra de vida, justo encima de ella) / coordenadas del mapa / píxeles de pantalla (para el narrador) | | `name` | Quién habla, en la primera línea; texto libre, no tiene por qué ser esa unidad | | `text` | Texto, con salto de línea automático | | `duration_ms` | Cuánto tiempo se muestra; 0 = automático, 3 ~ 5 segundos | | `key` | Número de una burbuja de mundo / pantalla; un mensaje nuevo con la misma key sustituye al anterior | | `update` | Si ya existe esa burbuja, solo cambia el texto sin reiniciar el temporizador (para streaming) | | `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… | Como máximo 32 burbujas a la vez; el coste medio por fotograma es de unos 0.1 ~ 0.2 ms. ### Conversar con un modelo local `POST /api/chat` ```json { "inst": 16, "unit": "0x14A12614", "name": "Rey de la Montaña", "persona": "Eres Muradin Barbabronce, el Rey de la Montaña de Warcraft: campechano y amante de la cerveza. Responde con una o dos frases coloquiales, de no más de 40 caracteres.", "message": "Hay un grupo de ogros delante, ¿cargamos o no?", "stream": true } ``` Devuelve `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, y para entonces la respuesta ya está sobre la cabeza de esa unidad. Cada unidad recuerda las últimas 6 rondas de conversación. ### Otros | Endpoint | Descripción | |---|---| | `GET /api/instances` | Partidas en curso | | `GET /api/units?inst=16&mine=true&heroes=true` | Lista de unidades (con nombre en chino, coordenadas y vida) | | `POST /api/clear` | Borra una burbuja o todas | | `GET /api/llm`, `POST /api/llm` | Ver / cambiar la configuración del modelo (`base_url`, `model`, `max_tokens`, `temperature`) | | `POST /api/banter` | Tertulia de campesinos: los trabajadores de la base se quejan por turnos según su personaje, con una presentación al inicio (todos los datos de la partida son reales) | | `POST /api/camtalk` | Diálogos de cámara: los héroes y sus acompañantes que salen en cámara conversan según su identidad | | `POST /api/events` | Reacciones a la partida: empieza un combate, termina, cae un héroe, se sube de tier, destruyen un edificio… solo hablan cuando pasa algo | ## Cómo elegir un modelo local Medido en una RTX 5090 (5 frases de diálogo del juego): | Modelo | VRAM | Velocidad | Una respuesta | Conclusión | |---|---|---|---|---| | **Qwen3.6-35B-A3B** (MoE, solo 3B activos por paso), Q4, sin razonamiento | 20.6 GB | Aprox. 142 token/s | **Aprox. 0.3 s** (primer token en aprox. 0.27 s) | Recomendado: rápido y con un rol natural en chino | | gpt-oss-20b (MXFP4), razonamiento low | 11.3 GB | Aprox. 280 token/s | 0.3 ~ 0.8 s | Si vas justo de VRAM; en chino, algo plano | | Qwen3.6-27B (denso), Q4 | 17.2 GB | Aprox. 39 token/s | A los 5.5 s seguía pensando | No sirve para diálogo en tiempo real | - **La velocidad depende de los "parámetros activos por paso", no del total**: el MoE de 35B solo activa 3B y es 3 ~ 4 veces más rápido que el denso de 27B. - **Hay que desactivar el "razonamiento"**: si no, todos los tokens se van en pensar y no responde nada. - Las burbujas escriben unos 22 caracteres por segundo, así que la velocidad de generación ya no es el cuello de botella; lo que de verdad marca la experiencia es la **latencia del primer token**. > **Que los diálogos parezcan reales** > > Pasa siempre al modelo datos reales de la partida (partidas jugadas, victorias y derrotas, tropas, recursos) y dile claramente "usa solo estos hechos". En las pruebas, sin esa restricción, el modelo se inventaba combates que nunca ocurrieron. --- # Pasarela > Pasarela WebSocket / JSON: las interfaces públicas que puede llamar el SDK de Python también las pueden llamar JS, C#, Go, Rust, páginas web o programas en otra máquina. Tres roles, con cliente JS y página de demostración incluidos; la latencia es la del carril rápido más ~1 ms. La pasarela envuelve el carril rápido y el estado publicado en **WebSocket / JSON**. Las interfaces públicas del [Catálogo de API](https://war3ai.com/es/api/) que puede llamar el SDK de Python también las pueden llamar JS, C#, Go, Rust, páginas web, programas en otra máquina y LLM, con los mismos nombres de método y los mismos parámetros. La latencia es la del carril rápido más ~1 ms. **Lo más cómodo: en la página de inicio de Farsight, "Centro de control" → "Pasarela" → "Iniciar"** (en esa misma tarjeta también puedes detenerla, reiniciarla, ver el log y abrir la página de demostración). Desde la línea de comandos: ```bash python gateway/server.py # ws://127.0.0.1:8870/ws (el puerto está en ports.gateway de openwar3.json) python gateway/server.py --open # lo mismo, y cuando el puerto ya escucha abre la página de demostración http://127.0.0.1:8870/demo python gateway/server.py --host 0.0.0.0 # para la LAN: exige token automáticamente (bin/gateway/token.txt) python gateway/server.py --allow-origin http://localhost:5173 # para que también se conecte tu propia página web ``` ## Conexión y roles Dirección de conexión: `ws://127.0.0.1:8870/ws?inst=9&role=dev` (también puedes usar `pid=` en lugar de `inst=`; si hace falta token, añade `&token=`). | Rol | Qué puede llamar | Ideal para | |---|---|---| | `dev` | Todo: observación, comandos, control del juego, sandbox (cambiar el mundo con JASS), dibujar interfaz | Herramientas locales, [mods de juego](https://war3ai.com/es/docs/mods/), compañeros | | `player` (con `&player=N`) | Observación, dar órdenes a las unidades del jugador N, dibujar interfaz; **modo justo por defecto**: solo ve lo que está en la visión del jugador N (`&fair=0` lo desactiva) | Bots o LLM que juegan por un jugador | | `observer` | Solo lectura (el runtime rechaza directamente los comandos que envíe) | Espectadores, comentaristas, recogida de datos | Lo que `player` no puede usar: el control del juego, como terminar la partida, cambiar la velocidad o pausar; `players` y `enemy_ai_plan`, que dejan ver las cartas de los demás; `canvas.image`, que hace que el proceso del juego abra archivos locales; y JASS. Las consultas que llevan número de jugador, como `resources`, `tech` o `stats`, solo pueden consultar el propio. Cada conexión es una sesión y ocupa un carril rápido (el runtime tiene 16 en total). La pasarela admite como máximo 12 sesiones a la vez, para dejar algunos carriles a los Bots, los mods y Farsight. Al desconectarse solo se retira lo que dibujó esa sesión y sus atajos de teclado; lo que dibujaron otros programas no se toca. ## Mensajes Al conectar, lo primero que llega es `hello`: versión del protocolo, rol, ID del proceso del juego y la lista de métodos que ese rol puede llamar. Después, cada petición lleva un `id` y la respuesta lleva el mismo `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", "Comprar poción"], "kwargs": {"screen": [40, 300]}} → {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true} ← {"type": "state", ...} {"type": "events", ...} a partir de aquí se envían continuamente → {"id": 5, "op": "overview"} la partida en una página: recursos, unidades por tipo, héroes, enemigos visibles, producción → {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"} solo para dev → {"id": 7, "op": "api"} catálogo de métodos (también hay ping / unsubscribe) ``` - **Parámetros de unidad**: se escriben `{"unit": dirección}`, donde la dirección es el `addr` del JSON de la unidad; puedes añadir `"handle": [lo, hi]` para comprobar que esa dirección no la ha reutilizado otra unidad. - **Nombres de método**: son los métodos públicos de Game, más `ui.*` (button / choice / toast / hotkey / mouse / cursor…), `canvas.*` (text / panel / bar / image / circle / path / remove…) y `jass.` (solo para dev). - Un cliente remoto no puede aportar funciones de callback: los clics y los atajos de teclado se reciben en los eventos enviados, y el evento `ui.click` lleva la `key`. Ver [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/). - Si una llamada falla, solo se responde con error a esa llamada (`ok: false` más `error`) y la conexión sigue abierta; lo mismo si lo que se envía no es JSON. - Los campos del JSON de eventos coinciden con el [Protocolo W3P](https://war3ai.com/es/docs/protocol/), con campos de conveniencia adicionales (`spell`, `key`, `text`, `chat`, `button`, `player`, `mods`). También funciona por HTTP, útil para llamadas sueltas y para curl: `GET /api?role=player` lista el catálogo de métodos y `POST /call` con `inst`, `role`, `method`, `args` y `kwargs` hace una llamada. `/call` reutiliza la sesión: si el juego se reinicia y cambia de proceso, pasa automáticamente a una nueva, y las que llevan 10 minutos inactivas se cierran. ## Clientes **JS** (navegador o Node 22+, sin dependencias): `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", "Púlsame", { screen: [40, 300] }); // el último objeto plano = argumentos con nombre ow.on("event:ui.click", (e) => console.log("Pulsado", e.key)); await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true }); ``` Con Node 20 / 21 hay que añadir `--experimental-websocket`. El ejemplo completo está en `gateway/clients/js/example.mjs`. **Página de demostración en el navegador** `http://127.0.0.1:8870/demo`: la partida, la tabla de unidades propias, poner un botón en el juego y el flujo de eventos, todo en una página. **Otros lenguajes**: basta con cualquier biblioteca de WebSocket + el JSON de arriba; no hace falta tocar la memoria compartida. **LLM**: usa directamente el [servidor MCP](https://war3ai.com/es/docs/mcp/), que convierte las tareas habituales en herramientas listas para usar. ## Medido 2026-09-25, verificación punto por punto conectada a una partida real, 16/16 (9 de la pasarela + 7 de MCP): handshake (rol dev, 121 métodos), `units('me')`, la partida en una página, aviso en pantalla, poner un botón; tras suscribirse, clic en ese botón dentro del juego → `ui.click` llega al cliente; JASS; pasar una unidad no válida solo da error en esa llamada; HTTP `/call` (rol observer). También se probaron el cliente JS (Node) y la página de demostración: un botón puesto desde la web recibe un clic en el juego y el registro de eventos de la web recibe `ui.click`. ## Seguridad - Por defecto solo escucha en `127.0.0.1` y no pide token (igual que Farsight). Si `--host` no es una dirección local, exige token automáticamente; `--auth` lo exige también en local. - **Otros sitios web abiertos en el navegador no pueden conectarse**: todas las conexiones que inicia un navegador llevan su origen (`Origin`), y la pasarela solo acepta su propia página de demostración y las direcciones dadas con `--allow-origin`. Programas como Python, Node o curl no envían origen y se conectan con normalidad. Cuando solo escucha en local, además comprueba `Host`, lo que bloquea los ataques que hacen resolver un dominio externo a la máquina local. - El rol lo declara el propio cliente al conectar: en modo local es una convención, no una frontera de seguridad. En la Arena, el proceso árbitro decidirá qué rol recibe cada uno; ver [Arena](https://war3ai.com/es/arena/). --- # Protocolo W3P > Todo el contrato entre el runtime y los programas externos: ocho bloques de memoria compartida, lectura del estado del mundo, lectura de eventos, envío de comandos, recibos, roles de carril, lienzo e interfaz y entrada. Si te integras desde un lenguaje distinto de Python, lee esta página. El runtime y los programas externos intercambian datos **solo a través de memoria compartida**. Lo que sigue es todo lo que hay. - La implementación de referencia es la de Python: `sdk/python/w3world.py` (lectura) y `sdk/python/w3fast.py` (escritura); ahí están definidos el tamaño y los offsets de cada estructura, fijados por tests; - **El protocolo solo describe semántica y no depende de la versión del juego.** Cuando cambia la versión del juego, el runtime se adapta solo y el protocolo no cambia; los campos nuevos solo se añaden al final de un bloque, así que los clientes antiguos siguen funcionando. > **Nota** > > La mayoría no necesita leer esta página: basta con usar el SDK de Python. Solo la necesitas si quieres integrarte directamente desde C++ / C# / Rust / Go u otro lenguaje, o si quieres saber qué ocurre por debajo del SDK. ## 1. Ocho bloques de memoria compartida `` es el ID del proceso del juego. | Nombre | Dirección | Contenido | Sincronización | |---|---|---|---| | `Local\War3World_` | runtime → tú | Estado del mundo: cabecera + 16 jugadores + hasta 1024 unidades + 256 detalles de unidad + 256 objetos en el suelo + zona de extensión + tabla de producción | seqlock | | `Local\War3Trees_` | runtime → tú | Hasta 4096 destructibles (árboles, etc.), se refresca cada 2 segundos | seqlock | | `Local\War3Events_` | runtime → tú | Anillo de eventos, 8192 entradas | cada entrada lleva su propio número de secuencia | | `Local\War3Map_` | runtime → tú | Mapa: celdas de terreno (128 por celda, hasta 256×256) + límites del área jugable + puntos de inicio; se calcula por lotes durante los primeros segundos de la partida | seqlock (no cambia una vez calculado) | | `Local\War3Fast_` | bidireccional | Carriles de comandos: 16 carriles × 16 slots; cada slot contiene un comando + su recibo; cada carril tiene un rol | un escritor y un lector por slot | | `Local\War3Canvas_` | tú → runtime | [Lienzo](https://war3ai.com/es/docs/canvas/): cabecera de 64 bytes + 256 elementos × 112 bytes + 64 KB de pool de texto / puntos; solo se crea después de enviar una vez `canvas_enable` | seqlock (tú escribes, el runtime lee en cada fotograma) | | `Local\War3Msgs_` | runtime → tú | Anillo de mensajes en pantalla: texto completo de avisos del juego, chat y mensajes del sistema, 128 entradas × 256 bytes | cada entrada lleva su propio número de secuencia | | `Local\War3Input_` | bidireccional | [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/): el runtime escribe la posición del ratón, el punto del suelo al que apunta y el elemento bajo el cursor; tú escribes la tabla de atajos de teclado y los interruptores del ratón; el runtime solo empieza a capturar la entrada después de enviar una vez `input_enable` | seqlock para la tabla de atajos | **Varios clientes usando el lienzo y la entrada a la vez**: de estos dos bloques solo hay una copia, y si cada uno escribe por su cuenta se sobrescriben entre sí. Esta es la convención, y tu propio cliente también tiene que seguirla: - **Lienzo**: con el mutex con nombre `Local\War3CanvasMutex_` tomado, haz lectura - modificación - escritura, cambia solo tus propios elementos y deja intactos los de los demás (recolocando los offsets del pool); elimina los que no tienen dueño y los de dueños cuyo proceso ya terminó. En cada elemento, `reserved[1]` = ID del proceso dueño y `reserved[2]` = número de secuencia dentro del proceso; los números de elemento se asignan desde el contador en el offset 60 de la cabecera del bloque (empezando en `0x10000`). - **Entrada**: cada cliente registra sus atajos de teclado y sus interruptores del ratón en `Local\War3InputClients_` (cabecera de 16 bytes + 16 clientes × 528 bytes); con `Local\War3InputMutex_` tomado, actualiza su propia entrada y luego escribe en el bloque de entrada la combinación de todos los clientes vivos: los atajos se deduplican por «código de tecla + modificadores» y los interruptores del ratón se unen. Los eventos se envían a todos los clientes, y cada uno reconoce sus atajos por «código de tecla + modificadores». Mientras queden otros clientes vivos en la tabla de registro, no envíes `input_enable 0`. - **Runtime**: los elementos clicables cuyo proceso dueño ya terminó dejan de interceptar clics; cada 2 segundos revisa la tabla de registro y, si todos los clientes registrados han terminado, pone a cero la tabla de atajos y los interruptores del ratón del bloque de entrada. ## 2. Leer el estado del mundo (seqlock) ```text loop: s1 = block.seq (offset 8, int32) if s1 es impar: reintentar (el runtime está escribiendo) copiar cabecera + players + units[unitCount] + details[detailCount] + items[itemCount] if block.seq != s1: reintentar ``` - **Cabecera**: contador de publicaciones (si no aumenta, la publicación se ha cortado), reloj de juego del motor, un epoch que suma 1 en cada partida, número del jugador local, si hay una partida en curso, velocidad de juego, periodo de publicación, microsegundos que costó recopilar esta copia en el hilo del juego, número de secuencia de eventos y tiempos por etapa. El cliente puede escribir `requestedPeriodMs` para pedir un periodo de publicación (16 ~ 1000 ms). - **Unidad** (112 bytes): par de handles (**identifica las unidades por el par de handles**: las direcciones se reutilizan), código de cuatro caracteres del tipo, dueño, flags, coordenadas, vida / maná (con sus máximos), orden actual + objetivo de la orden, objetivo de tarea (a quién ataca realmente), nivel / experiencia / puntos de habilidad del héroe, índice de detalle, `visibleTo` (bit p = el jugador p la ve en este momento). - **Detalle** (288 bytes; héroes > unidades de jugador > creeps; hasta 256): 12 habilidades (código / nivel / flags / segundos de enfriamiento restantes), 8 códigos de buff, 6 casillas de inventario. - **Extensiones al final del bloque** (solo se añaden; los offsets anteriores nunca se mueven, así que los clientes antiguos siguen funcionando): zona de extensión `EXT1` (hora del día dentro del juego, velocidad del ciclo día/noche, número de entradas de la tabla de producción) y tabla de producción `prods[128]` (edificios que están entrenando / investigando / construyendo / mejorando, cola, duración total, tiempo transcurrido, si está atascada). **Úsalas solo si el magic coincide.** ## 3. Leer eventos ```text head = ring.writeSeq (offset 8) for seq in (cursor, head]: e = ring.events[(seq - 1) % 8192] if e.seq > seq: se perdió una (sobrescrita por leer demasiado despacio) elif e.seq != seq: aún no está escrita del todo, léela la próxima vez else: procesar e ``` La estructura de evento ocupa 64 bytes: `seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra`. - Obtenidos comparando dos publicaciones consecutivas (precisión = periodo de publicación): `unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started`; - A nivel de motor (el runtime los registra en el hilo del juego en el momento en que ocurren, así que hay uno por **cada golpe**): `damage` (origen, tipo de daño, tipo de ataque, posición, vida perdida real, daño antes de armadura), `killed` (el asesino); - Obtenido siguiendo la tabla de producción: `production.done` (código de cuatro caracteres de lo terminado, categoría, cuántos segundos de juego tardó; también se emite para los rivales); - Comprobados por el runtime en cada publicación: `spell.cast` (la habilidad entra en enfriamiento: `a` código de cuatro caracteres de la habilidad, `b` nivel, `value` segundos de enfriamiento, `x/y` punto de lanzamiento), `player.left` (`a` número de jugador, `b` nuevo estado de la ranura), `selection.changed` (selección del jugador local; la tabla completa está en la zona de extensión del bloque del mundo), `game.ended` (se sale de la partida); - Mensajes en pantalla: `message` (`a` = número de secuencia del mensaje; el texto completo se busca en la memoria compartida `Local\War3Msgs_`: 128 entradas × 256 bytes, con avisos del juego, chat y mensajes del sistema; `b` = número del marco de mensajes); - Interfaz y entrada (después de `input_enable`): `ui.click` (`a` id del elemento del lienzo, `b` 1 botón izquierdo / 2 botón derecho), `ui.hover`, `hotkey` (`a` id del atajo, `b` código de tecla virtual), `mouse.world` (`x/y` coordenadas del suelo, `value` = 1 significa que el clic se tragó), con las teclas modificadoras siempre en `extra`. ## 4. Enviar comandos 1. **Cada objeto cliente ocupa un carril**: con `Local\War3FastMutex_` tomado, busca un carril libre (o cuyo proceso dueño haya muerto) y escribe el rol, el número de jugador y tu propio pid. Si un mismo proceso necesita dos roles, abre dos carriles; 2. Rellena los slots: flag de comando semántico, código de operación, `args[11]` y plazo límite `deadlineMs`; 3. Cuando todos los slots estén escritos, márcalos como enviados y suma 1 al `submitSeq` del carril; 4. Espera al evento `Local\War3FastDone__` (o haz polling), lee los recibos y devuelve los slots. El runtime ejecuta los comandos por lotes dentro del despacho de eventos del hilo del juego: cada vaciado tiene un presupuesto de **4 ms** (temporizador real de alta precisión); lo que no cabe se deja para el siguiente despacho. **Los slots que han superado su plazo nunca se ejecutan**, así que no ocurre que «al reanudar tras una pausa se vuelvan a ejecutar comandos viejos». Índices de `args`: `0..2` unidad (dirección, handle lo, handle hi), `3` ID de orden o código de cuatro caracteres, `4..6` objetivo, `7/8` x / y (bits de float), `9` extra (número de jugador / número de casilla / interruptor / modo de cola), `10` mode (0 sin objetivo / 1 a un punto / 2 a un objetivo). ### Códigos de operación | Código | Nombre | Descripción | |---|---|---| | 1 | `point` | Orden de una unidad a un punto (mover / atacar-mover / patrullar / atacar el suelo / hechizo a un punto). extra bit0 = en cola (se inserta tras la orden actual) | | 2 | `target` | Orden de una unidad a un objetivo (atacar con clic derecho / recolectar / reparar / hechizo a una unidad / recoger objeto); el objetivo tiene que ser visible | | 3 | `immediate` | Comando sin objetivo (detener / mantener posición / entrenar / investigar / mejorar / hechizo sin objetivo) | | 4 | `build` | Un trabajador construye un edificio (coordenadas alineadas a 32) | | 5 | `learn` | El héroe aprende una habilidad | | 6 | `use_item` | Usar la casilla extra del inventario | | 7 | `revive` | Revivir un héroe en el altar | | 8 | `rally` | Punto de reunión (a un punto / a un objetivo) | | 9 | `buy` | Una tienda vende un objeto al héroe que está al lado | | 10 | `item_drop` | Soltar un objeto: dárselo a un aliado, vendérselo a una tienda (`code` = la unidad que lo recibe) o dejarlo en el suelo | | 20 ~ 25 | Consultas | `q_tech` recuento de tecnología, `q_feasible` viabilidad, `q_visible` visibilidad, `q_mine_gold` oro restante en una mina, `q_captain` capitán del ordenador, `q_dead_heroes` lista de héroes muertos | | 30 | `pause` | Pausar / reanudar | | 40 ~ 50 | Cámara | Leer el estado de la cámara, fijar un campo, mirar a un punto, seguir, restablecer, rotar, límites, suavizado, mostrar / ocultar la interfaz, imagen limpia, niebla | | 60 ~ 63 | HUD | Texto del botón de misiones, título y descripción del panel de misiones, refrescar, leer si el panel está abierto | | 70 | `jass` | Llama a una native de JASS por su nombre (1291 en total): el nombre y los parámetros de tipo string van en la zona adicional del slot, el resto de parámetros se colocan en `args` según la firma; el valor de retorno queda en `value[0]`. Solo para carriles de herramientas locales; se rechaza cualquier llamada con parámetros de tipo función o que pueda suspender el hilo de scripts. Ver [Canal JASS](https://war3ai.com/es/docs/jass/) | | 71 / 72 | `jass_handle_of` / `jass_unit_of` | Convierte entre unidad de la instantánea ↔ handle de JASS (el par de handles de la instantánea no es un handle de JASS) | | 73 | `canvas_enable` | Crea la memoria compartida del lienzo e instala el hook de dibujo; cualquier carril puede enviarlo (el lienzo solo se dibuja en la pantalla local). La primera vez hay que instalar el hook: da un timeout de 2 segundos o más | | 74 | `input_enable` | `extra` = 1 captura la entrada de la ventana del juego (clic / hover sobre elementos del lienzo, atajos de teclado, clics en el suelo); 0 = la devuelve. Bloque de entrada `Local\War3Input_`: cabecera de 128 bytes + 32 atajos × 16 bytes; tú escribes la tabla de atajos y los interruptores del ratón, y el runtime escribe la posición del ratón, el punto del suelo al que apunta y el elemento bajo el cursor. Cualquier carril puede enviarlo (solo afecta a la entrada local). Ver [Interfaz y entrada](https://war3ai.com/es/docs/ui-input/) | ## 5. Recibos Un recibo ocupa 52 bytes (+8 bytes de tiempos): `status`, `engineReturn`, `verdict` (código de motivo del rechazo), `orderBefore / orderAfter` (la orden de la unidad leída en el mismo frame), `value[8]` (resultados de consultas), `execUs` (microsegundos que tardó este comando en ejecutarse en el hilo del juego), `engineUs` (la parte que corresponde a la propia función de órdenes del motor). Todos los códigos de estado y de motivo están en [Recibos y códigos de motivo](https://war3ai.com/es/docs/reason-codes/). ## 6. Roles de carril | Rol | Qué puede hacer | |---|---| | `dev` | Herramientas locales: comandos semánticos (mandar las unidades del jugador local) + canal JASS | | `player` | Solo comandos semánticos, y solo para las unidades del jugador al que pertenece el carril (las de otro = `not_owner`) | | `observer` | Solo consultas, cámara, lectura del estado de los paneles del HUD y activar el lienzo y la entrada local; todo lo demás es `forbidden` | Dos IA enfrentadas = dos carriles `player` en la misma partida (player 0 / player 1). > **Atención** > > En modo local, el rol lo declara el propio cliente (es una convención, no una frontera de seguridad). En la [Arena](https://war3ai.com/es/arena/), el proceso árbitro crea los carriles y solo entrega el carril `player` a cada participante. ## 7. Semántica verificada en partidas reales - Clic derecho (smart) sobre un enemigo = atacar **a ese en concreto** (el objetivo de la orden y el objetivo de tarea son esa unidad); la orden de ataque en bruto enviada como comando a un objetivo solo cambia a la orden de ataque sin recordar el objetivo, así que la unidad se va a atacar a otro cercano; - El motor no permite comandos a un objetivo sobre unidades que no ves: al anochecer, los campamentos lejanos quedan en la niebla de guerra y todos los clics derechos se rechazan (1001); - Que una construcción sea «aceptada» solo significa que el trabajador recibió la orden: un punto dentro de un bosque también se acepta en el acto, y el trabajador solo falla al llegar; los puntos claramente ocupados se rechazan en el acto; - Un héroe solo puede revivirse unos 3 segundos de juego después de morir; también se rechaza si no hay comida suficiente (los héroes ocupan comida); - Los objetos del inventario no cuentan como objetos en el suelo; al recoger uno se emite `item.removed`; - Durante la pausa el reloj del motor se detiene, pero se pueden seguir enviando comandos; - Una partida iniciada minimizada tiene la simulación detenida (el reloj no avanza). --- # Recibos y códigos de motivo > El recibo de cada comando incluye un código de estado y un código de motivo. Son la base para que Bots y agentes se corrijan solos: convierten "por qué no se hizo" en un número legible por máquina. ```python r = g.train(barracks, "hfoo") bool(r) # False r.status # 1 -> rejected r.verdict # 3 -> comida insuficiente r.reason # 'rejected(人口不够)' r.exec_us # microsegundos que tardó en ejecutarse en el hilo del juego ``` `if r:` equivale a `r.status == 0` (el motor lo aceptó). ## Códigos de estado `status` | Código | Nombre | Significado | Causas habituales | |---|---|---|---| | 0 | `accepted` | El motor lo aceptó | — (pero aceptado ≠ hecho; ver abajo) | | 1 | `rejected` | El motor lo rechazó | Mira `verdict` | | 2 | `bad_unit` | La unidad no existe o el handle no coincide | La unidad ya murió; usaste un objeto de unidad caducado | | 3 | `not_owner` | No es tu unidad | Mandar unidades ajenas con el rol `player` | | 4 | `fault` | Excepción durante la ejecución (el runtime la contuvo; no tumba el juego) | Repórtalo con los pasos para reproducirlo | | 5 | `bad_args` | Parámetros incorrectos | Coordenadas, índice de casilla o código de cuatro caracteres mal escritos | | 6 | `unsupported` | No soportado | Esta versión del runtime no tiene esa capacidad | | 7 | `bad_target` | Objetivo no válido | El objetivo ya no existe; el tipo de objetivo no es el correcto | | 8 | `forbidden` | El rol del carril no lo permite | Dar órdenes con el rol `observer` | | 97 | `cancelled` | Se lanzó una excepción dentro del bloque de lote y no se envió nada del lote | El código dentro de `with g.batch():` dio error | | 98 | `held` | Una capa de mayor prioridad retiene la unidad y no se envió | La capa de reflejos del cerebro de referencia o las órdenes manuales de la consola tienen esa unidad | | 99 | `timeout` | Tiempo agotado | Con el juego en pausa o atascado se superó el plazo (los comandos caducados no se ejecutan) | ## Códigos de motivo `verdict` Cuando hay un rechazo, el runtime explica el motivo con la propia comprobación de viabilidad del motor. También puedes preguntar antes, sin dar la orden: `g.can_do(unidad, código)` devuelve los mismos códigos. | Código | Significado | Qué hacer | |---|---|---| | 0 / 220 | Se puede | — | | 3 | Comida insuficiente | Construye edificios de comida; `g.production(b).blocked` lo detecta antes | | 8 | Oro insuficiente | Espera a tener oro; antes de ordenar, usa `g.can_afford(code)` | | 9 | Madera insuficiente | Manda más trabajadores a talar | | 32 | Cola de entrenamiento llena (7 huecos) | Solo 1 en la cola: pon el siguiente cuando `g.queue(b)` esté vacía | | 183 | Falta una tecnología / edificio previo | Construye primero el edificio necesario o sube de tier | | 185 | Edificio ocupado | El altar está reviviendo a un héroe; el ayuntamiento no puede mejorarse con la cola ocupada | | 221 | No existe / en construcción / mejorándose / ya existe | Ya tienes ese héroe (si murió, usa `revive`); esta tienda no vende eso | | 89 | La tienda aún no tiene existencias | Al inicio de la partida, cada objeto aparece según su tiempo de reposición en la tabla de objetos; en una tienda recién construida, se cuenta desde que se termina | | 1001 | Objetivo no visible | El objetivo está en la niebla o en la máscara negra; usa `attack_move` hacia su posición | ## Aceptado ≠ hecho El recibo solo indica que "el motor aceptó el comando" y se lee en el mismo fotograma. No cubre lo que pueda pasar después: | Comando | Puede fallar aunque se haya aceptado | Cómo confirmarlo | |---|---|---| | Construir | Un punto dentro de un bosque también se acepta al momento; falla cuando llega el trabajador | Usa `build_near` (comprueba si aparecen los cimientos) o espera a `production.done` | | Hechizo | Lo interrumpen o falta maná | En el siguiente tick, comprueba si `g.cooldown(u, habilidad)` ha entrado en enfriamiento | | Entrenar | Entra en la cola, pero sin comida nunca empieza | `g.production(b).blocked` | | Mover / atacar | Otra lógica (o una capa de mayor prioridad) lo cambia | `g.current_target(u)`, `g.order_of(u)` | ## Interfaces de consulta Estas interfaces no dan órdenes; solo preguntan al motor. El resultado también va en el `value` del recibo (el SDK devuelve el valor directamente): | Interfaz | Devuelve | |---|---| | `g.can_do(u, code)` / `g.can_do_many([(u, code), ...])` | Los códigos de motivo de la tabla anterior | | `g.tech(code, player=None)` / `g.tech_many([...])` | Nivel de investigación / número de edificios terminados (cuenta la cadena de mejoras) | | `g.visible(x, y)` | Si tu bando ve ese punto | | `g.gold_left(mine)` | Cuánto oro le queda a la mina | | `g.enemy_ai_plan(unidad_enemiga)` | Adónde va a llevar sus tropas el capitán de la IA rival (solo funciona con la IA del juego) | --- # De dónde salen los datos > Origen y precisión de cada tipo de dato. Si pasa algo que "parece raro", mira primero esta página. | Dato | Origen | Precisión | |---|---|---| | Unidades, recursos, órdenes, habilidades, buffs, inventario | Bloque del mundo que el runtime publica cada 50 ms | Ciclo de publicación (ajustable a 16 ms) | | Eventos de daño y de muerte | El runtime los registra en el hilo del juego, golpe a golpe | Inmediata | | Otros eventos (aparición, muerte, cambio de orden, subida de nivel…) | Comparación entre dos publicaciones consecutivas | Ciclo de publicación | | Tabla de producción (entrenamiento / investigación / construcción / mejora) | Campos de temporizador de las habilidades de producción del motor + tiempo transcurrido que acumula el runtime | Aprox. ±0.2 segundos de juego | | Estadísticas de combate, tabla de tipos | Tablas de datos del propio juego (extraídas de tu copia local) | Sin los modificadores de objetos, auras ni buffs | | Rutas | Transitabilidad del terreno del motor (celdas de 128) + árboles + huella de los edificios, A* en el lado del SDK | Una celda; los huecos más estrechos que una celda se consideran bloqueados | | Hora del juego | JASS `GetFloatGameState(GAME_STATE_TIME_OF_DAY)` | Ciclo de publicación | | Visibilidad | Máscara de visibilidad por jugador que el runtime calcula para cada unidad | Ciclo de publicación | | Niveles de tecnología, viabilidad, oro restante en la mina | Consultas por el carril rápido, directamente al motor | Inmediata | ## Los datos del juego no se distribuyen con el código Las tablas de unidades, habilidades, objetos, héroes, buffs, tipos de daño y armadura, etc., proceden de los archivos del juego de Blizzard y **no están en el repositorio**. Cuando configuras el directorio del juego en el "Centro de control" de Farsight, se extraen automáticamente de tu propia copia del juego; también puedes ejecutarlo a mano: ```bash python data/tools/extract_game_data.py ``` El resultado se guarda en `data/game/` (fuera de git): los `.slk` / `.txt` originales y, ya procesados, `units.json`, `names.json`, `skills.json`, `items.json`, `heroes.json` y `buffs.json`. ## Algunas cifras concretas | Magnitud | Valor | |---|---| | Un día | 480 segundos de juego (240 de día y 240 de noche); una hora = 20 segundos de juego; la partida empieza a las 8 de la mañana | | Día | 6:00 ~ 18:00 | | Coeficiente de armadura | 0.06 (de las tablas de datos del juego) | | Capacidad del bloque del mundo | 16 jugadores, 1024 unidades, 256 detalles de unidad, 256 objetos en el suelo, 128 producciones | | Árboles | Hasta 4096 destructibles, actualizados cada 2 segundos | | Anillo de eventos | 8192 entradas; si lees demasiado lento se pierden (el SDK lo detecta) | | Celdas del mapa | 128 unidades de juego por celda, como máximo 256 × 256 | ## Ejemplos calibrados con mediciones - Tiempos de producción: campesino 14.9, granja 34.9 y Espadas de hierro forjado 59.9 segundos de juego, idénticos a lo que publica el runtime (error inferior a 0.2 s); - Estadísticas de combate comprobadas contra el panel del juego: Paladín con 650 de vida, 255 de maná, 3.9 de armadura y ataque 24 ~ 34; un soldado con la mejora de ataque de nivel 1, 13 ~ 15; - El daño antes de armadura de los eventos de daño del motor (14 / 15 / 15) cae dentro del rango que calcula `stats()`; - Rutas: Echo Isles, 116 × 88 celdas; distancia por tierra hasta el ayuntamiento rival 10642 (en línea recta 9856); construir la malla lleva 18 ms y un A*, aprox. 1 ms. --- # Preguntas frecuentes > ¿Es un hack? ¿Qué versiones admite? ¿Qué ve y qué puede hacer la IA? ¿Puedo usarlo sin saber programar?… ## ¿Es un hack? No. Es una interfaz de desarrollo para investigación y entretenimiento con IA, y solo se usa con **un cliente que poseas legalmente**, para enfrentarse a la IA del juego o a otras IA en tu equipo, en LAN o en partidas propias. **No debe usarse en Battle.net ni en ningún servidor con antitrampas**, y no ofrece ninguna función pensada para partidas contra personas. Más detalles en [Límites de uso](https://war3ai.com/es/docs/legal/). ## ¿Qué versiones del juego admite? Por ahora solo **Warcraft III 1.27** (The Frozen Throne). Las versiones 1.24 ~ 1.28 comparten la misma estructura de motor; el soporte multiversión (tabla de símbolos por versión, firmas de respaldo, autocomprobación al arrancar con lista de capacidades) es la fase P4 de la [hoja de ruta](https://war3ai.com/es/roadmap/). Las versiones a partir de 1.29 y Reforged usan otro motor, necesitan una adaptación aparte y, por ahora, no están comprometidas. ## ¿Modifica mis archivos del juego? No. El runtime se inyecta mientras el juego se ejecuta y **no modifica Game.dll en disco** ni ningún otro archivo del juego. Para abrir varias instancias solo copia y renombra el lanzador original `War3.exe`, sin tocarlo. Los datos del juego (tablas de unidades, etc.) se extraen de tu propia copia y no se distribuyen con el código. ## ¿Qué puede ver la IA? Básicamente todo lo que querría saber un jugador profesional, actualizado cada 50 ms: - Oro, madera y comida de todos los jugadores; posición, vida y maná, orden actual, **a quién ataca**, nivel y experiencia de todas las unidades; - Nivel y enfriamiento restante de las habilidades de héroes y unidades, sus buffs e inventario; - Qué entrena / investiga / construye / mejora cada edificio, cuánto progreso lleva y si está bloqueado por falta de comida; - Objetos en el suelo, árboles, la malla transitable / edificable del mapa, puntos de inicio y hora del juego (día y noche); - Flujo de eventos: unidades que aparecen y mueren, **cada golpe de daño** (quién lo dio, tipo de ataque, daño antes de armadura), muertes, producción completada, subidas de nivel de héroes… - También puede preguntar directamente al motor: si algo se puede hacer ahora y, si no, por qué; qué nivel tiene una tecnología; si un punto es visible; cuánto oro le queda a una mina; hacia dónde va a atacar la IA rival. Además, el SDK ya calcula las estadísticas de combate (tabla de tipos, armadura, mejoras de ataque y armadura), "cuántos segundos se tarda en matarlo" y las rutas por tierra. Todas las interfaces están en el [catálogo de API](https://war3ai.com/es/api/). ## ¿Qué puede hacer la IA? Prácticamente todo lo que puede hacer un jugador: mover, avanzar atacando, atacar un objetivo concreto, detenerse, mantener posición, patrullar, atacar al suelo, recolectar, reparar, construir (con búsqueda automática de ubicación), entrenar / investigar / mejorar, cancelar, aprender habilidades, lanzar hechizos (sobre unidad / sobre punto / sin objetivo), punto de reunión, revivir héroes, recoger / usar / soltar / dar / vender objetos, comprar, Llamada a las armas; cola con Shift, marchar por waypoints, que un trabajador construya varios edificios seguidos; y además velocidad de juego, pausa y burbujas de diálogo. Cada comando tiene su recibo. Además de las acciones del jugador, puede dibujar tus propios paneles y marcas sobre la pantalla del juego ([lienzo](https://war3ai.com/es/docs/canvas/)) y, en partidas de un jugador, llamar a las 1291 funciones JASS que usan los autores de mapas ([canal JASS](https://war3ai.com/es/docs/jass/)). ## ¿Se puede usar en mapas RPG / personalizados? Sí. Elige un mapa RPG, asigna a la instancia el esquema "Ejemplo de compañero" y, al empezar la partida, juegas tú: a tu lado irá un compañero de IA que te ayuda a combatir, te cura y charla contigo; ver [Compañero para RPG](https://war3ai.com/es/docs/companion/). `g.map_data` lee los nombres de las unidades personalizadas del mapa; el [canal JASS](https://war3ai.com/es/docs/jass/) puede crear unidades, fijar aliados, mostrar paneles… Cómo jugar lo decides tú. Las operaciones que cambian el mundo solo están disponibles en partidas de un jugador (en multijugador se desincronizan); el lienzo es seguro también en multijugador. ## ¿Puedo usarlo sin saber programar? Sí. Prepara el entorno con el [Inicio rápido](https://war3ai.com/es/docs/quickstart/) y luego sigue [Escribe un Bot con un LLM](https://war3ai.com/es/docs/ai-bot/): tú describes la estrategia con tus palabras y el LLM escribe el código; si algo falla al ejecutarlo, pásale el error o lo que ves en el juego y pídele que lo corrija. ## ¿Solo funciona con Python? El SDK es de Python. Entre el runtime y los programas externos solo hay un protocolo de memoria compartida ([W3P](https://war3ai.com/es/docs/protocol/)), así que cualquier lenguaje que lea y escriba memoria compartida de Windows puede conectarse. Más cómodo todavía es la [pasarela](https://war3ai.com/es/docs/gateway/) (WebSocket / JSON): JS, C#, Go, Rust, páginas web y programas en otra máquina pueden llamar a las mismas interfaces; un agente LLM puede conectarse directamente por [MCP](https://war3ai.com/es/docs/mcp/). ## ¿Qué LLM es el mejor? Sirve cualquier modelo de referencia que sepa escribir código. La clave no es el modelo, sino **darle el material correcto** (manual + `api.json` + un ejemplo) y exigirle que use solo métodos que existan en el catálogo de API. Las decisiones en tiempo real durante la partida (asesor, voces) son sensibles a la latencia, y los modelos MoE locales rinden muy bien; consulta [El LLM como asesor](https://war3ai.com/es/docs/llm-coach/) y [Burbujas y modelos locales](https://war3ai.com/es/docs/speech/). ## ¿Ralentiza el juego? Una recogida del estado del mundo tarda en el hilo del juego una mediana de 0.5 ~ 0.9 ms (100 ~ 120 unidades), una vez cada 50 ms. Cada comando tarda unos microsegundos en el hilo del juego; cada vaciado tiene un presupuesto de 4 ms y lo que no se termina pasa al siguiente, así que no frena el juego. Todas las llamadas al juego están protegidas contra excepciones: si un Bot se cae, solo se detiene ese bando, sin tumbar el juego. ## ¿Puedo abrir varias partidas a la vez? Sí. `runtime/farm.py` orquesta varias instancias, cada una con su número, y se arrancan y detienen desde la [consola Farsight](https://war3ai.com/es/docs/console/). Tu Bot se conecta a una instancia concreta con `--inst N`. ## ¿Puedo enfrentar a dos IA? Abre dos canales `player` en la misma partida (`--player 0` / `--player 1`) y tendrás IA contra IA. En modo local, la justicia depende de un acuerdo entre las partes; los enfrentamientos oficiales con árbitro, filtrado por visión y validación de propiedad llegarán con la [Arena](https://war3ai.com/es/arena/) (fase P6). ## ¿Funciona en Mac / Linux? Por ahora solo en Windows 10 / 11. ## ¿Qué licencia tiene? La licencia se publicará con la versión oficial. Los componentes de terceros conservan sus propias licencias (por ejemplo, MinHook es BSD-2); AMAI tiene una licencia propia y sus datos derivados no se distribuyen con el proyecto: se descargan del repositorio público de AMAI y se generan durante la instalación. ## ¿Dónde reporto problemas? Tras el lanzamiento oficial se abrirá un canal para reportar problemas. Cuando lo hagas, incluye el número de instancia, la salida de `python -m openwar3 status` y los pasos para reproducirlo. Antes, mira si [Depuración y rendimiento](https://war3ai.com/es/docs/debugging/) lo resuelve. --- # Límites de uso > Qué puedes hacer y qué no, las estadísticas de visitas de este sitio web y los avisos sobre marcas y licencias de terceros. Al usar este proyecto aceptas respetar estos límites. ## Permitido - Usarlo en un cliente de Warcraft III 1.27 **que poseas legalmente**; - Enfrentar IA contra la IA del juego o contra otras IA en tu equipo, sin conexión, en LAN o en partidas propias; - Investigación, enseñanza, entretenimiento y retransmisión de las partidas de tus propias IA; - Desarrollos derivados basados en el SDK, el cerebro de referencia, los ejemplos y las herramientas, respetando sus licencias. ## No permitido - **No debe usarse en Battle.net ni en ningún servidor o plataforma con antitrampas**, ni al mismo tiempo que una sesión activa de un sistema antitrampas; - No debe usarse para obtener ventajas indebidas en partidas contra personas; - No se deben distribuir archivos del juego de Blizzard ni datos extraídos de ellos (este proyecto tampoco lo hace: cada usuario extrae los datos de su propia copia del juego); - Debe respetarse la licencia de uso del runtime. ## Nuestros compromisos técnicos - No se modifica `Game.dll` en disco ni ningún archivo del juego; todos los cambios ocurren en tiempo de ejecución; - Para abrir varias instancias solo se copia y renombra el lanzador original `War3.exe`, sin tocarlo; - El proyecto no contiene código ni archivos del juego de Blizzard. ## Tu responsabilidad Las leyes sobre ingeniería inversa y modificación de juegos varían según el país o la región. **Cada usuario debe comprobar si usar este proyecto es legal donde se encuentra y asume las consecuencias de su uso.** Este proyecto se ofrece "tal cual", sin garantías de ningún tipo, ni expresas ni implícitas. ## Estadísticas de visitas de este sitio web Este sitio web (war3ai.com) usa Microsoft Clarity para medir las visitas: qué páginas se ven, de dónde llegan los visitantes, cuánto tiempo se quedan, dónde hacen clic y hasta dónde se desplazan, además de grabaciones anónimas de la navegación y mapas de calor. Solo lo usamos para mejorar la documentación y las páginas. - No hace falta registrarse y no se recoge información de identidad como el nombre o el correo electrónico; el texto de los campos de entrada se oculta por defecto y no se registra; - Clarity guarda cookies en el navegador para distinguir las distintas visitas de un mismo visitante; los datos los trata Microsoft, ver la [Declaración de privacidad de Microsoft](https://privacy.microsoft.com/privacystatement); - Si no quieres que se te contabilice: abre una vez cualquier dirección de este sitio con `?stats=off` añadido al final y ese navegador dejará de contabilizarse desde entonces (`?stats=on` lo restablece); también puedes bloquear `clarity.ms` con la función de bloqueo de rastreo del navegador, y el sitio seguirá funcionando con normalidad. Farsight, el SDK y el runtime que se ejecutan en tu equipo no llevan este tipo de estadísticas. Farsight solo se conecta a war3ai.com en dos casos: al arrancar y después cada 6 horas, para leer la lista de versiones y ver si hay una nueva; y cuando pulsas enviar en la página "Comentarios y sugerencias", para mandar el comentario que has escrito y un identificador del equipo (un hash con sal del identificador del sistema, del que no se puede recuperar el valor original; sirve para evitar envíos masivos). La información de diagnóstico solo se adjunta si marcas la casilla, y puedes previsualizarla antes de enviarla. Cuando estas dos solicitudes llegan a war3ai.com, el servidor registra la dirección IP, el país o región que determina Cloudflare y la versión del cliente (User-Agent), para evitar abusos y contabilizar cuántas instancias de Farsight están en uso. Los registros de las comprobaciones de versión se eliminan automáticamente después de 90 días; los comentarios, junto con esta información, se conservan hasta que el mantenedor los procese y elimine. Estos datos solo son visibles para el mantenedor del proyecto en el backend, y no se comparten con nadie más. ## Marcas Warcraft® es una marca comercial o marca registrada de Blizzard Entertainment, Inc. War3AI / OpenWar3 es un proyecto comunitario independiente, sin afiliación con Blizzard Entertainment y sin su respaldo ni patrocinio. Los demás nombres de productos mencionados (Claude, GPT, Gemini, Qwen, etc.) pertenecen a sus respectivos propietarios y solo se citan para indicar compatibilidad. ## Componentes y datos de terceros | Componente / datos | Licencia | Tratamiento | |---|---|---| | MinHook | BSD-2-Clause | Se usa con el runtime, conservando su aviso de licencia | | AMAI | Licencia propia | No se distribuye con el proyecto; al desplegar, `start.bat` lo descarga del repositorio público de AMAI y una herramienta genera a partir de él los datos que usa el cerebro de referencia | | Datos del juego (unidades, habilidades, objetos, etc.) | Blizzard | No se distribuyen con el proyecto; cada usuario los extrae de su propia copia del juego | | Datos factuales extraídos de repeticiones de partidas públicas (ubicación de edificios, órdenes de apertura) | — | Solo datos factuales, que usa el cerebro de referencia | --- # Catálogo de API (api.json) Estado: verified = la ruta de bajo nivel se ha verificado en partidas reales; experimental = interfaz nueva, ya funciona y se está verificando punto por punto en partidas reales; inferred = inferido / no probado del todo. Latencia:Snapshot publicado(Lee memoria compartida sin esperar al hilo del juego (~0.05 ms)); Carril rápido(~1 fotograma: ejecución por lotes en el hilo del juego); Canal de control(20~40 ms (ruta antigua para operaciones de interfaz)); Escritura directa(Sin cola en el hilo del juego: escribe en memoria compartida (lienzo) o envía un mensaje a la ventana del juego); Cálculo local(Cálculo puro o lectura de archivos, no toca el juego) ## Observación Leen el estado sin cambiar el juego. Casi todas leen directamente el snapshot publicado, sin espera. - `snapshot(max_age: 'float' = 0.05)` [verified] [Snapshot publicado] Estado completo de todo el mapa (WorldState): .units .players .items .clock .me; las llamadas repetidas dentro de max_age segundos devuelven la misma copia. ⚠ Los trabajadores que están dentro de una mina de oro no aparecen; por defecto se ve todo el mapa (con el modelo lockstep, el cliente local lo tiene todo); solo Game(fair=True) filtra según la visión. (Mecanismo: Bloque del mundo W3P Local\War3World_ (el runtime lo publica cada 50 ms, seqlock)) - `last_seen(owner: 'str | int' = 'enemy', max_age: 'float | None' = None) -> 'list'` [verified] [Snapshot publicado] Unidades enemigas (o 'creep' para creeps, o un número de jugador) vistas por última vez: [(la unidad tal como era entonces, reloj de juego en ese momento, segundos transcurridos)], las más recientes primero. Si la ves morir, se borra de la lista. Tanto en modo justo como en modo normal se registra según "lo que vemos en este momento": es el mapa que un jugador lleva en la cabeza: el ejército que ha explorado, dónde vio por última vez al héroe rival, cuándo abrió el rival su expansión. max_age: solo las de los últimos tantos segundos de juego. (Mecanismo: visibleTo de la instantánea push (en cada refresco de la instantánea se registran las unidades enemigas / creeps visibles)) - `map()` [verified] [Snapshot publicado] Tabla de terreno de esta partida, MapInfo: .walkable(x,y) .buildable(x,y) .at(x,y) .bounds (área jugable) .starts (puntos de inicio) .cells (bit0 no transitable, bit1 no edificable). Tarda unos segundos en calcularse al empezar la partida; hasta entonces devuelve None. Los árboles no están incluidos (usa trees()). (Mecanismo: Bloque de mapa W3P Local\War3Map_ (el runtime lo calcula por lotes al empezar la partida; IsTerrainPathable para caminar / construir)) - `me() -> 'int | None'` [verified] [Snapshot publicado] Qué número de jugador soy (0~11). (Mecanismo: cabecera del bloque del mundo) - `resources(player: 'int | None' = None) -> 'dict | None'` [verified] [Snapshot publicado] {'gold','lumber','food_used','food_cap','gold_gathered','lumber_gathered'}; player es nuestro jugador por defecto; se puede leer el de cualquier jugador. Si no se puede leer devuelve None; no lo trates como 0. (Mecanismo: bloque del mundo players[16]) - `players() -> 'list'` [verified] [Snapshot publicado] Los 16 slots de jugador: Player(id, gold, lumber, food_used, food_cap, gold_gathered, lumber_gathered, race, known). (Mecanismo: bloque del mundo players[16]) - `units(owner: 'str | int' = 'all', types=None, alive: 'bool' = True) -> 'list'` [verified] [Snapshot publicado] Filtra unidades por dueño / tipo. owner: 'me' / 'enemy' / 'creep' / 'all' / número de jugador. types: conjunto de códigos de cuatro caracteres. (Mecanismo: bloque del mundo units[]) - `unit(handle) -> 'object | None'` [verified] [Snapshot publicado] Busca una unidad por su par de handles (lo, hi) (el objetivo de la orden, el objetivo de tarea y los eventos dan pares de handles). (Mecanismo: bloque del mundo by_handle) - `is_building(u) -> 'bool'` [verified] [Snapshot publicado] Si es un edificio (torres incluidas). Se decide por velocidad de movimiento 0 en la tabla de unidades; la huella del edificio principal de los No-muertos es 0, así que no lo decidas por la huella. (Mecanismo: instantánea + units.json (spd==0 = edificio)) - `my_workers() -> 'list'` [verified] [Snapshot publicado] Nuestros trabajadores (campesino / peón / acólito / fuego fatuo). (Mecanismo: instantánea push) - `idle_workers() -> 'list'` [verified] [Snapshot publicado] Trabajadores sin nada que hacer: sin orden y sin tarea (no cuentan los que acabas de mandar a trabajar en este tick). ⚠ Volver a dar la orden de recolectar a un trabajador que tiene tarea interrumpe el ciclo de recolección (los ingresos caen a cero). (Mecanismo: instantánea push (slot de orden + slot de tarea)) - `my_heroes() -> 'list'` [verified] [Snapshot publicado] Nuestros héroes vivos (los muertos están en la lista de resurrección del altar; ver revive). (Mecanismo: instantánea push) - `my_army() -> 'list'` [verified] [Snapshot publicado] Nuestras unidades de combate: ni trabajadores ni edificios. (Mecanismo: instantánea push + units.json) - `my_buildings(types=None) -> 'list'` [verified] [Snapshot publicado] Nuestros edificios (torres y cimientos en construcción incluidos); con types puedes pedir solo algunos tipos, p. ej. {'hbar'}. (Mecanismo: instantánea push) - `is_constructing(worker) -> 'bool'` [verified] [Snapshot publicado] Si este trabajador está construyendo (o va de camino a construir / está ayudando a reparar; incluye lo que acabas de ordenar en este tick). Sáltalo al elegir constructor; si no, los cimientos anteriores se quedarán parados. (Mecanismo: instantánea push (orden = código de cuatro caracteres de un edificio, u orden de construir / reparar)) - `under_construction(building) -> 'bool'` [verified] [Snapshot publicado] Este edificio aún no está terminado (vida no llena). ⚠ Un edificio dañado tampoco tiene la vida llena: basta para la apertura, pero una vez empiezan los combates hay que tener en cuenta también el tiempo. (Mecanismo: instantánea push (la vida de los cimientos sube desde muy poco hasta el máximo)) - `gold_mines() -> 'list'` [verified] [Snapshot publicado] Las minas de oro del mapa. ⚠ La mina de oro enredada de los Elfos de la noche y la mina neutral son dos unidades en las mismas coordenadas; manda a recolectar a la tuya. (Mecanismo: instantánea push (ngol/egol/ugol)) - `enemies(fighters_only: 'bool' = False) -> 'list'` [verified] [Snapshot publicado] Unidades de los jugadores enemigos (sin creeps). fighters_only: quita trabajadores y edificios. (Mecanismo: instantánea push) - `creeps() -> 'list'` [verified] [Snapshot publicado] Creeps (neutral hostil). ⚠ De noche la visión se reduce; cuando un campamento lejano queda en la niebla de guerra, los comandos dirigidos a él se rechazan (código de motivo 1001). (Mecanismo: instantánea push (owner 12 = neutral hostil)) - `life_mana(u) -> 'dict | None'` [verified] [Snapshot publicado] {'hp','hp_max','mana','mana_max'} (float, valores en bruto del motor). Para u basta con la unidad obtenida de la instantánea (se sustituye por la copia más reciente). (Mecanismo: unidad del bloque del mundo hp/hpMax/mana/manaMax) - `hero_info(hero) -> 'dict | None'` [verified] [Snapshot publicado] {'level','xp','skill_points'}. (Mecanismo: unidad del bloque del mundo level/xp/skillPoints) - `abilities(u) -> 'list'` [verified] [Snapshot publicado] [{code, level, cooldown, flags}]; los buffs están en buffs(u). Solo las unidades "con detalle" lo tienen (héroes > unidades de jugador > creeps, hasta 256). (Mecanismo: detalle del bloque del mundo: habilidades (código / nivel / flags / enfriamiento restante)) - `buffs(u) -> 'list'` [verified] [Snapshot publicado] Códigos de buff que tiene la unidad (p. ej. 'BHds' Escudo divino, 'Bslo' Ralentizar). Qué efecto corresponde a cada código: ver data/game/buffs.json. (Mecanismo: detalle del bloque del mundo: objetos de habilidad que empiezan por B) - `cooldown(u, ability: 'str') -> 'float | None'` [verified] [Snapshot publicado] Segundos de enfriamiento que le quedan a esta habilidad (segundos de juego); 0 = se puede lanzar; si la unidad no tiene esa habilidad (o no tiene detalle) devuelve None. (Mecanismo: detalle del bloque del mundo: enfriamiento restante de la habilidad (temporizador de la habilidad)) - `inventory(hero) -> 'list | None'` [verified] [Snapshot publicado] Códigos de cuatro caracteres de los objetos de las 6 casillas (casilla vacía = None); sin inventario devuelve None. (Mecanismo: detalle del bloque del mundo: inventario de 6 casillas) - `current_order(u) -> 'dict | None'` [verified] [Snapshot publicado] {'order','target','x','y'}: la orden que la unidad está ejecutando (order es 0x000D00xx o el código de cuatro caracteres de un edificio; 0 = ociosa). target es un par de handles; conviértelo en unidad con g.unit(target). (Mecanismo: unidad del bloque del mundo: order / objetivo de la orden / punto objetivo de la orden) - `current_target(u)` [verified] [Snapshot publicado] La unidad a la que **realmente ataca / persigue** (si no hay, devuelve None). ⚠ Tras una orden de ataque, el slot de orden se vacía enseguida y el ataque queda en la tarea: para saber "a quién ataca" usa esto, no current_order. (Mecanismo: unidad del bloque del mundo: objetivo de tarea) - `clock() -> 'float | None'` [verified] [Snapshot publicado] Reloj de juego del motor (segundos de juego; 0 durante la carga). A velocidad aumentada avanza más rápido que el reloj real. (Mecanismo: cabecera del bloque del mundo clockMs (reloj de juego del motor)) - `production(building)` [verified] [Snapshot publicado] Qué está haciendo este edificio: Production(kind, queue, duration, elapsed, blocked, progress, remaining…); si no hace nada, devuelve None. kind 'queue' (entrenamiento / investigación / héroe; queue tiene como máximo 7 casillas, [0] es lo que se está haciendo) / 'construction' (en construcción) / 'upgrade' (mejora del edificio principal / de una torre); blocked = hay cola pero no ha empezado (casi siempre falta comida: toca construir granjas); progress 0..1. También sirve para los edificios del rival (en modo justo, solo para los edificios visibles). (Mecanismo: tabla de producción del bloque del mundo (objetos de habilidad Aque/ABnP/AUnP + tiempo transcurrido que sigue el runtime; error medido < 0.2 segundos de juego)) - `queue(building) -> 'list'` [verified] [Snapshot publicado] Códigos de cuatro caracteres de la cola de entrenamiento / investigación ([0] es lo que se está haciendo); ocioso o no es un edificio de producción = []. (Mecanismo: tabla de producción del bloque del mundo) - `all_production(owner: 'str | int' = 'me') -> 'list'` [verified] [Snapshot publicado] Toda la producción en curso [(edificio, Production)]. owner igual que en units(): 'me' / 'enemy' / número de jugador / 'all'. Uso profesional: ver qué unidades entrena el rival, qué tecnologías investiga y cuándo sube de tier (cuando has explorado sus edificios). (Mecanismo: tabla de producción del bloque del mundo) - `path_distance(a, b) -> 'float | None'` [verified] [Snapshot publicado] Distancia que recorre una unidad terrestre de a a b (a y b pueden ser unidades o (x,y)); si no se puede llegar, None. En mapas con islas, úsalo para saber "si a este campamento de creeps / esta expansión se llega por tierra"; es más fiable que la distancia en línea recta (rodea bosques, acantilados y edificios). Precisión de una celda de 128; los huecos más estrechos que una celda se consideran cerrados. (Mecanismo: bloque de mapa (IsTerrainPathable del motor) + bloque de árboles + huella de los edificios; A* en el lado del SDK (celdas de 128)) - `reachable(a, b) -> 'bool | None'` [verified] [Snapshot publicado] Si se puede llegar por tierra (bloque de mapa aún sin calcular = None). (Mecanismo: igual que el anterior) - `walk_path(a, b) -> 'list | None'` [verified] [Snapshot publicado] Puntos de giro del camino [(x,y)...] (el último es b); combínalo con path(units, lista_de_puntos) para que las tropas sigan ese camino (esquivar torres, ir por atajos). (Mecanismo: igual que el anterior) - `upkeep(player: 'int | None' = None) -> 'dict | None'` [inferred] [Snapshot publicado] Nivel de mantenimiento: {'level': 'none'/'low'/'high', 'income': 1.0/0.7/0.4, 'next_at': comida del siguiente nivel (si no hay = None)}. Sabiduría profesional: quédate en 50 de comida mientras subes a tier 3 / investigas mejoras de ataque y armadura, y sube a 80 solo antes del combate decisivo. (Mecanismo: regla fija de la 1.27: 0~50 de comida sin mantenimiento, 51~80 ingresos ×0.7, 81~100 ×0.4) - `xp_to_next(hero) -> 'int | None'` [verified] [Snapshot publicado] Experiencia que le falta al héroe para el siguiente nivel (nivel 10 = 0). (Mecanismo: bloque del mundo level/xp + fórmula NeedHeroXP de MiscGame) - `creep_camps(link: 'float' = 600.0) -> 'list'` [verified] [Snapshot publicado] Agrupa en campamentos los creeps (visibles) del mapa: [{'x','y','units','level','hp','max_level'}], de más cerca a más lejos de nuestra base principal. level = nivel total del campamento (la medida habitual de la dificultad del creeping), hp = vida total. Combínalo con time_to_kill / path_distance para elegir campamento. (Mecanismo: instantánea push (creeps a menos de 600 entre sí agrupados en un campamento) + nivel de units.json) - `buff_info(code: 'str') -> 'dict | None'` [verified] [Cálculo local] Qué es un código de buff: {'ability','effect','dur','hero_dur','targets'} (p. ej. 'Bslo' -> Ralentizar). Si un código tiene varias filas, devuelve la primera. (Mecanismo: data/game/buffs.json (BuffID de AbilityData.slk -> habilidad / efecto / duración)) - `stats(u, player: 'int | None' = None)` [verified] [Snapshot publicado] Atributos de combate de la unidad, combat.UnitStats: vida / maná máximos, armadura (incluye mejoras de ataque y armadura y la agilidad del héroe), tipo de armadura, velocidad de movimiento, visión de día / de noche, armas (a qué puede atacar, alcance, intervalo de ataque, rango de daño, tipo de ataque, salpicadura). u puede ser una unidad (usa automáticamente las tecnologías de su dueño y el nivel del héroe) o un código de cuatro caracteres (player es nuestro jugador por defecto). Combínalo con .dps_vs(rival) / .hits_to_kill(rival) / combat.time_to_kill(grupo, rival). ⚠ No incluye objetos, auras ni buffs. (Mecanismo: tablas de datos (UnitBalance/UnitWeapons/UpgradeData/MiscGame) + niveles de tecnología en tiempo real + nivel del héroe) - `time_to_kill(attackers, target) -> 'float | None'` [verified] [Snapshot publicado] Cuántos segundos de juego tarda este grupo de unidades en matar a target atacando juntas (usa la vida actual de target; tiene en cuenta counters, armadura y mejoras de ataque y armadura; no tiene en cuenta el movimiento, la salpicadura ni la curación). Uso profesional: al concentrar fuego, ataca primero al que "muere más rápido" (el menor time_to_kill), no al más cercano. Si no se le puede atacar = None. (Mecanismo: stats() + vida en tiempo real) - `time_of_day() -> 'float | None'` [verified] [Snapshot publicado] Hora del día dentro del juego (horas, 0~24). La partida empieza a las 8 de la mañana; un día completo = 480 segundos de juego (240 s de día y 240 s de noche, escalados por la velocidad del ciclo día/noche). Si no se puede leer (runtime antiguo / fuera de partida) devuelve None. (Mecanismo: zona de extensión del bloque del mundo: GetFloatGameState(GAME_STATE_TIME_OF_DAY)) - `is_night() -> 'bool | None'` [verified] [Snapshot publicado] Si ahora es de noche (18:00~6:00). Jugada profesional: de noche los creeps duermen (atacas primero sin que te rodeen) y la visión de todas las unidades se reduce (buen momento para emboscadas); los centinelas / unidades de los Elfos de la noche se vuelven invisibles de noche junto a los árboles. Si no se puede leer devuelve None. (Mecanismo: zona de extensión del bloque del mundo (de día de 6 a 18 h)) - `seconds_until(hour: 'float') -> 'float | None'` [verified] [Snapshot publicado] Segundos de juego que faltan para que el reloj del juego marque la hora hour (p. ej. seconds_until(18) = cuánto falta para el anochecer; útil para planificar el creeping nocturno). (Mecanismo: zona de extensión del bloque del mundo + día de 480 s (medido: 20 segundos de juego por hora)) - `items_on_ground() -> 'list'` [verified] [Snapshot publicado] Objetos en el suelo [Item(addr, handle_lo, handle_hi, type, x, y, life)]. Al recogerlos / usarlos se emite el evento item.removed. (Mecanismo: bloque del mundo items[] (solo los del suelo: handle del portador todo FF)) - `trees(x: 'float | None' = None, y: 'float | None' = None, limit: 'int' = 60) -> 'list'` [verified] [Snapshot publicado] Árboles vivos (los que tienen tree en targType de DestructableData); si das (x,y), se ordenan de más cerca a más lejos, como máximo limit. Cada uno es un Tree(addr, handle_lo, handle_hi, type, x, y, life) y se puede pasar directamente a gather para talar. (Mecanismo: bloque de árboles Local\War3Trees_ (se refresca cada 2 segundos)) - `events() -> 'list'` [verified] [Snapshot publicado] Lo que ha pasado desde la última llamada: unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started (estos salen de comparar publicaciones; precisión = periodo de publicación, 50 ms), además de damage / killed a nivel de motor (el runtime los registra en el hilo del juego en el momento en que ocurren, así que hay uno por **cada golpe**): damage: handle = quien recibe el golpe, .source_addr = quien golpea (conviértelo en unidad con snapshot().unit_by_addr), .value = vida perdida real, .raw_damage = daño antes de armadura, .attack_type (normal/pierce/siege/magic/chaos/hero/spell), .damage_type killed: este golpe lo mató, .source_addr = el asesino y production.done, que el runtime obtiene siguiendo la tabla de producción (precisión = periodo de publicación): unidad = el edificio, .done_code = código de cuatro caracteres de lo terminado, .done_kind = 'training' (unidad / héroe / resurrección) / 'research' / 'construction' (edificio terminado) / 'upgrade' (subida de tier / mejora de torre), .value = segundos de juego que tardó Añadidos el 09-25: spell.cast: unidad = quien lanza, .spell código de cuatro caracteres de la habilidad, b nivel, value segundos de enfriamiento, x,y punto de lanzamiento (se detecta cuando la habilidad entra en enfriamiento; precisión = periodo de publicación) player.left: .player número del jugador que se fue / fue eliminado por derrota; game.ended: se sale de la partida selection.changed: cambió la selección del jugador local (obtén las unidades con g.selection()) message: una línea de un marco de mensajes de la pantalla (avisos del juego, chat, sistema): .text texto completo, .frame número del marco de mensajes, .chat = {'channel', 'sender', 'text'} (si es chat; lo que el jugador escribe en el chat se lee de aquí) ui.click / ui.hover / hotkey / mouse.world: interfaz y entrada (g.ui); .key es la key del lienzo / el atajo tal como se escribió Cada uno es un Event(seq, kind, clock, addr, handle, type, owner, a, b, x, y, value, extra). En modo justo (fair=True) solo llegan: eventos de tus propias unidades, eventos de unidades visibles en este momento (o que lo eran hace menos de 1 segundo), el daño que recibimos / que hacemos, y los eventos locales de interfaz / mensajes / partida. (Mecanismo: anillo de eventos Local\War3Events_ (comparación de publicaciones + eventos de daño capturados por el runtime)) - `selection() -> 'list'` [verified] [Snapshot publicado] Unidades que tiene seleccionadas ahora el jugador local (la unidad principal primero; como máximo 12). Cuando cambia la selección se emite el evento selection.changed. (Mecanismo: zona de extensión del bloque del mundo W3P, selAddrs (el runtime incluye la selección del jugador local en cada publicación)) - `messages() -> 'list'` [verified] [Snapshot publicado] Mensajes nuevos en los marcos de mensajes de la pantalla desde la última llamada: [{'text', 'frame', 'repeat', 'seq', 'game_ms'}]. Aquí están los avisos del juego («Necesitas más granjas», «No se puede construir ahí»), el chat y los mensajes del sistema; frame indica de qué marco de mensajes se trata. Son los mismos que los eventos message del flujo de eventos (cada uno con su propio cursor). (Mecanismo: memoria compartida Local\War3Msgs_ (mensajes en pantalla capturados por el runtime)) - `tech(code: 'str', player: 'int | None' = None) -> 'int | None'` [verified] [Carril rápido] Nivel de investigación / número de edificios terminados (cuenta la cadena de mejoras: un Castillo también cuenta como htow). player es nuestro jugador por defecto; se puede consultar cualquier jugador. (Mecanismo: consulta W3P q_tech (recuento de tecnología del jugador en el motor)) - `can_do(u, code: 'str') -> 'int | None'` [verified] [Carril rápido] Veredicto de viabilidad del motor: 0/220 se puede ordenar; 3 comida, 8 falta oro, 9 falta madera, 32 cola llena, 183 falta un requisito previo, 185 el altar está reviviendo, 221 no existe esa opción / en construcción. ⚠ Para un trabajador que construye un edificio siempre da 221; no sirve para comprobar dónde colocarlo (usa build_near). (Mecanismo: consulta W3P q_feasible (comprobación de viabilidad del motor)) - `can_do_many(pairs) -> 'list'` [verified] [Carril rápido] Muchos can_do de una vez: pairs = [(unidad, código de cuatro caracteres), ...]; devuelve la lista de códigos de veredicto en el mismo orden (los que no se pudieron consultar son None). Al planificar qué construir / entrenar en un tick, pregúntalo todo junto primero: es N veces más rápido que ir can_do por can_do (cerebro de referencia, 09-23: planificación de construcción de 76 -> 25 ms). (Mecanismo: consulta W3P q_feasible × N, enviadas en un lote) - `tech_many(codes, player: 'int | None' = None) -> 'dict'` [verified] [Carril rápido] Consulta de una vez muchos recuentos de tecnología / edificios: {código de cuatro caracteres: cantidad o None}. (Mecanismo: consulta W3P q_tech × N, enviadas en un lote) - `visible(x: 'float', y: 'float') -> 'bool | None'` [verified] [Carril rápido] Si ahora vemos este punto (no está en la niebla de guerra / zona negra). Un bot en modo justo solo debería usar enemigos visibles. (Mecanismo: consulta W3P q_visible (visible / niebla de guerra / zona negra)) - `gold_left(mine) -> 'int | None'` [inferred] [Carril rápido] Cuánto oro le queda a la mina. (Mecanismo: consulta W3P q_mine_gold (oro restante en la mina según el motor)) - `enemy_ai_plan(enemy_unit) -> 'dict | None'` [verified] [Carril rápido] El capitán de la IA del ordenador: adónde lleva a sus tropas (antes de salir ya sabe qué parte de tu base va a atacar). Solo funciona contra rivales del ordenador; si la unidad no sigue a un capitán, devuelve None. (Mecanismo: consulta W3P q_captain (el capitán del ordenador al que sigue la unidad enemiga)) - `order_of(u) -> 'int | None'` [verified] [Snapshot publicado] La orden actual de la unidad, **incluida la que acabas de dar en este tick** (mientras la instantánea no se pone al día, usa la orden nueva del recibo). ⚠ Partida real del 09-23: hello_bot acababa de mandar a un campesino a construir una granja y, en el mismo tick, rush_bot lo vio "ocioso" en la instantánea y lo mandó a construir un cuartel; la granja se quedaba a medias una y otra vez. Para elegir unidades "ociosas / que no están construyendo", usa esto en lugar de u.order. (Mecanismo: orden de la instantánea + comandos de este proceso recién aceptados (recibos)) - `can_afford(code: 'str') -> 'bool'` [verified] [Snapshot publicado] Si el oro / la madera actuales alcanzan para comprar code (unidades, edificios; según los precios de units.json). Lo que no está en la tabla de precios se considera siempre asequible. ⚠ Los códigos de subida de tier tienen precio acumulado en la tabla, así que aquí el resultado es conservador; lo que cuenta al final es el recibo del motor. (Mecanismo: nuestros recursos de la instantánea push + precios de units.json) - `map_data()` [verified] [Cálculo local] Datos del mapa que se está jugando (openwar3.mapdata.MapData): name_of('HC07') da el nombre de unidades / objetos / habilidades personalizados; también hero_names y tooltip. En los mapas RPG la mayoría de las unidades las crea el propio mapa y no están en la tabla de nombres integrada; si la partida no la arrancó el lanzador (no se encuentra el archivo del mapa), devuelve None. (Mecanismo: archivo del mapa (la ruta --map del lanzador): w3u/w3t/w3a + wts; en mapas protegidos lee los TXT del propio mapa) ## Comandos Hacen que las unidades actúen. Se aplican en ~1 fotograma, cada uno con su recibo. - `batch() -> 'Batch'` [verified] [Carril rápido] Agrupa en un lote los comandos de un tick: with g.batch() as b: g.attack(archers, target) # devuelve Pending; se convierte en recibo al cerrar el bloque g.move(wounded, *home) g.cast(hero, "thunderclap") print(b.sent, b.wait_ms, [r.reason for r in b.receipts]) Cada comando enviado por separado espera una vez a que lo procese el hilo del juego (unos 10 ms); un lote espera una sola vez: así el cerebro de referencia (09-23) bajó una ronda de 48 -> 26 ms. * El arbitraje sigue pasando comando a comando (una unidad reservada recibe en el acto un recibo held y no entra en el lote); * Dentro del bloque los comandos devuelven Pending: leer su .ok antes de cerrar el bloque lanza un error (el recibo aún no existe); después se usa igual que un Receipt; * Una excepción dentro del bloque = se descarta el lote entero (status 97 cancelled) y se liberan las unidades reservadas; * Las consultas (can_do / tech / visible …), build_near y buy no entran en el lote y se preguntan en el acto, como siempre: su resultado se necesita al momento; para preguntar muchas cosas de una vez usa can_do_many / tech_many; * Un with g.batch() anidado se une al lote más externo; con más de 16 comandos, el runtime lo divide automáticamente en tramos (una espera por tramo). (Mecanismo: los comandos del bloque se acumulan en un lote que se envía de una vez al cerrar el bloque (se ejecutan en el mismo frame; una sola espera al hilo del juego)) - `move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Carril rápido] Ir a (x,y) sin atacar por el camino (úsalo para retirarte). Puedes pasar una unidad o una lista (reciben la orden juntas en el mismo frame). queue='after': ir después de terminar lo que está haciendo (se inserta tras la orden actual). values[0] del recibo = cuántas órdenes tiene en cola la unidad tras recibir esta (incluida la que está ejecutando). (Mecanismo: W3P point: move (bits de extra = modo de cola)) - `attack_move(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Carril rápido] Atacar-mover (A sobre el suelo): ataca a los enemigos que encuentre por el camino. queue igual que en move. (Mecanismo: W3P point: attack a un punto) - `attack(units, target, force: 'bool' = False, queue: 'str | None' = None)` [verified] [Carril rápido] Atacar a target. Por defecto usa el clic derecho (sobre un enemigo = atacar a ese en concreto; medido el 09-23: el objetivo de la orden y el objetivo de tarea son esa unidad). ⚠ El objetivo tiene que estar en tu campo de visión; si no se ve, se rechaza (código de motivo 1001). force=True usa la orden de ataque 0x0F (necesaria para atacar a unidades propias / animalillos neutrales). Medido: solo cambia a la orden de ataque sin recordar el objetivo, y la unidad se va a atacar a otro enemigo cercano; no la uses para atacar a un objetivo concreto. (Mecanismo: W3P target: comando a un objetivo (clic derecho, smart)) - `stop(units)` [verified] [Carril rápido] Detiene todo lo que esté haciendo (ID de orden 0x000D0004) y también vacía las órdenes en cola. (Mecanismo: W3P immediate: stop) - `hold(units, queue: 'str | None' = None)` [verified] [Carril rápido] Mantener posición (no persigue; solo ataca lo que está a su alcance). (Mecanismo: W3P immediate: holdposition) - `patrol(units, x: 'float', y: 'float', queue: 'str | None' = None)` [inferred] [Carril rápido] Patrulla entre la posición actual y (x,y). (Mecanismo: W3P point: patrol) - `attack_ground(units, x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Carril rápido] Atacar el suelo: la artillería dispara a una zona (contra unidades invisibles, contra lo que hay detrás de un bosque, para cerrar un paso). Solo lo aceptan las unidades que pueden atacar el suelo. (Mecanismo: W3P point: attackground (unidades de asedio / morteros / catapultas)) - `cancel(building)` [verified] [Carril rápido] Cancela: la última casilla de la cola de entrenamiento / investigación (devuelve el dinero), un edificio en construcción (devuelve el 75 %) o un edificio principal que se está mejorando. (Mecanismo: W3P immediate: cancel) - `path(units, points, attack: 'bool' = False)` [verified] [Carril rápido] Recorre una serie de puntos en orden (puntos encadenados con Shift: waypoints, esquivar torres, rutas de exploración). Con attack=True, cada tramo es un atacar-mover. Se envía de una vez; hay un recibo por punto (en el orden de points). (Mecanismo: un lote: el primer tramo se ejecuta al momento y el resto se inserta en orden inverso con queue='after' (el motor solo permite insertar tras la orden actual)) - `gather(workers, target, queue: 'str | None' = None)` [verified] [Carril rápido] Recolectar oro / talar (target es una mina de oro o un árbol de trees()). ⚠ Asígnalo solo a trabajadores ociosos (idle_workers): repetir la orden a uno con tarea interrumpe el ciclo de recolección. Uso profesional: volver a minar al terminar de construir = gather(worker, mine, queue='after') después de build(...). (Mecanismo: W3P target: harvest (mina de oro o árbol)) - `repair(workers, building, queue: 'str | None' = None)` [verified] [Carril rápido] Reparar / ayudar a construir (las obras de Humanos y Orcos se detienen si nadie construye). (Mecanismo: W3P target: repair) - `build(worker, code: 'str', x: 'float', y: 'float', queue: 'str | None' = None)` [verified] [Carril rápido] Hace que el trabajador construya code en (x,y) (coordenadas alineadas a 32). Recibo aceptado = la orden del trabajador ya es ese edificio (o la orden de empezar la obra); con queue='after' = entró en la cola de órdenes del trabajador (values[0] del recibo = número en cola). ⚠ Aceptado ≠ construido: el motor también acepta en el acto un punto dentro de un bosque, y el trabajador solo falla al llegar (medido el 09-23); si el dinero se gasta en otra cosa, los cimientos tampoco aparecen. Si no sabes dónde cabe, usa build_near (sigue el resultado y pone en la lista negra los puntos que fallan). Para construir varios seguidos, usa build_queue. (Mecanismo: W3P build: orden de construcción; se confirma leyendo la orden del trabajador en el mismo frame) - `build_queue(worker, plan)` [verified] [Carril rápido] Un trabajador construye varios edificios seguidos en orden (encadenados con Shift): plan = [(código de cuatro caracteres, x, y), ...]. Se envía de una vez; los recibos siguen el orden de plan. ⚠ El dinero se descuenta al empezar cada obra (no al encolar): si encolas 3 y solo tienes dinero para 1, los otros dos fallarán cuando el trabajador llegue. (Mecanismo: un lote: el primero al momento y el resto en orden inverso con queue='after') - `build_near(worker, code: 'str', x: 'float', y: 'float', min_r: 'float' = 450, max_r: 'float' = 1500, max_tries: 'int' = 24)` [verified] [Carril rápido] Busca alrededor de (x,y), de cerca a lejos, un punto donde quepa code y lo construye. **No bloquea**; puedes llamarlo en cada tick: * hay un intento de este edificio todavía en curso (el trabajador va de camino) -> devuelve ese punto y no repite la orden; * el intento anterior tuvo éxito (aparecieron los cimientos) -> esta vez busca un punto nuevo si hace falta; * el intento anterior falló (el trabajador llegó y vio que no cabía; el motor retiró la orden y no hay cimientos) -> ese punto va a la lista negra 45 segundos y se prueba el siguiente; * falta dinero -> devuelve None directamente (ni prueba ni pone en la lista negra); si se agotan los puntos, devuelve None. ⚠ Por qué hace falta el seguimiento: en partidas reales del 09-23, el motor **aceptaba en el acto** un punto dentro de un bosque y el trabajador solo fallaba al llegar (el recibo del mismo frame no puede detectarlo); y la comprobación de ubicación del motor siempre devuelve 221 para un trabajador que construye, así que tampoco se puede "consultar" antes de construir. Solo los puntos claramente ocupados (el centro del ayuntamiento) se rechazan en el acto. (Mecanismo: build punto a punto + seguimiento (aparecen los cimientos = éxito; el trabajador abandona la orden sin cimientos = ese punto a la lista negra)) - `train(building, code: 'str')` [verified] [Carril rápido] Entrena unidades / investiga tecnologías / mejora el edificio principal (subir de tier = dar al propio edificio principal el código de cuatro caracteres del edificio de destino, p. ej. 'hkee'). Si se rechaza, el reason del recibo dice por qué (falta comida, falta oro, falta madera, cola llena, falta un requisito previo…). (Mecanismo: W3P immediate: código de cuatro caracteres; si se rechaza, incluye el código de motivo de viabilidad) - `learn(hero, ability: 'str')` [verified] [Carril rápido] El héroe aprende una habilidad (código de cuatro caracteres, p. ej. 'AHbz' Ventisca). (Mecanismo: W3P learn: solo cuenta como aprendida si bajan los puntos de habilidad) - `cast(u, spell, target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Carril rápido] Lanza un hechizo. spell es un nombre de orden ('thunderbolt' Martillo de tormenta, 'blizzard', 'holybolt' Luz sagrada…; ver data/order-ids.txt) o un ID de orden. Con target = sobre una unidad; con x,y = sobre el suelo; sin ninguno = sin objetivo (Atronar, Escudo divino, Invocar elemental de agua). Que el recibo diga aceptado solo significa que el motor lo aceptó; para saber si se lanzó, mira si cooldown() entró en enfriamiento o si apareció algo en buffs(). (Mecanismo: W3P target / point / immediate (según los parámetros)) - `rally(building, x: 'float | None' = None, y: 'float | None' = None, target=None)` [verified] [Carril rápido] Fija el punto de reunión (en un punto, o sobre una unidad / mina de oro). (Mecanismo: W3P rally) - `revive(altar, hero=None)` [verified] [Carril rápido] Revive en el altar a un héroe muerto (si no das hero, revive al primero de la lista). Motivos de rechazo frecuentes (aparecen en el reason del recibo): falta comida (los héroes también ocupan comida), falta dinero, murió hace muy poco (solo puede revivir unos 3 segundos de juego después de morir), ya hay una resurrección en curso (al aceptarla, el motor vacía esa casilla en el acto). (Mecanismo: W3P revive: lista de héroes muertos -> el altar lanza la resurrección sobre el héroe muerto) - `pick_up(hero, item)` [verified] [Carril rápido] El héroe va a recoger un objeto del suelo (item sale de items_on_ground). Al recogerlo aparece en el inventario y se emite el evento item.removed para el suelo. (Mecanismo: W3P target: clic derecho sobre el objeto) - `use_item(hero, slot: 'int', target=None, x: 'float | None' = None, y: 'float | None' = None)` [verified] [Carril rápido] Usa el objeto de la casilla slot (0~5) del inventario; admite una unidad objetivo o un punto objetivo. ⚠ Al usar un objeto sobre un punto (p. ej. Ivory Tower), el motor devuelve 0 incluso si tiene éxito, así que el recibo siempre cuenta como aceptado: mira si esa casilla del inventario se vació. (Mecanismo: W3P use_item (por número de casilla)) - `drop_item(hero, slot: 'int', x: 'float', y: 'float')` [verified] [Carril rápido] Suelta en (x,y) lo que haya en la casilla slot del inventario (el héroe camina hasta allí y lo deja). (Mecanismo: W3P item_drop (copia de JASS UnitDropItemPoint: dropitem 0xD0021 a un punto + el objeto como objetivo inmediato)) - `give_item(hero, slot: 'int', to)` [verified] [Carril rápido] Da lo que haya en la casilla slot del inventario a to (otro héroe / unidad; camina hasta él y se lo entrega). Dárselo a una tienda = venderlo (ver sell_item). (Mecanismo: W3P item_drop (copia de JASS UnitDropItemTarget: dropitem sobre una unidad)) - `sell_item(hero, slot: 'int', shop)` [verified] [Carril rápido] Vende a una tienda lo que haya en la casilla slot del inventario (el héroe tiene que ir junto a la tienda; solo acepta objetos vendibles y devuelve la mitad del precio). (Mecanismo: igual que give_item, con una tienda como objetivo (medido: el Staff of Sanctuary se vende por 125 de oro)) - `move_item(hero, slot: 'int', to_slot: 'int')` [verified] [Carril rápido] Cambia de casilla dentro del inventario (de la casilla slot a la casilla to_slot; si ambas tienen algo, se intercambian). Útil para ordenar las teclas rápidas. (Mecanismo: W3P target: orden 0xD0022 + número de casilla, objetivo = el objeto (copia de JASS UnitDropItemSlot)) - `buy(shop, item_code: 'str')` [inferred] [Carril rápido] Compra un objeto en una tienda (para el héroe que está junto a la tienda). Si falta un requisito tecnológico, el motor devuelve 0 y no cobra. (Mecanismo: W3P buy: la tienda vende al héroe que está al lado) - `call_to_arms(hall, on: 'bool' = True)` [verified] [Carril rápido] Llamada a las armas de los Humanos: los campesinos se convierten en milicia (el ayuntamiento de tier 1 no tiene esta habilidad; solo funciona en el Torreón / Castillo). (Mecanismo: W3P immediate: townbellon/off) ## Control del juego Velocidad, pausa, ciclo de publicación, burbujas de diálogo, lienzo, interfaz y entrada, mensajes. - `ui()` [verified] [Escritura directa] Interfaz y entrada (openwar3.ui.UI): botones y tarjetas de elección clicables, atajos de teclado, clics en el suelo para elegir una posición, a qué apunta el ratón. El juego no recibe el clic sobre un botón; es solo entrada local + dibujo local, así que también es seguro en multijugador. (Mecanismo: W3P 74 input_enable + memoria compartida Local\War3Input_ (el runtime recibe la entrada de la ventana)) - `set_speed(percent: 'int') -> 'bool'` [verified] [Canal de control] Velocidad de juego (100 = velocidad normal). (Mecanismo: acción 47 (25~800%)) - `pause(on: 'bool' = True)` [verified] [Carril rápido] Pausa / reanuda el juego. Durante la pausa el reloj del motor se detiene, pero por el carril rápido se pueden seguir dando órdenes (el despacho de eventos sigue funcionando). (Mecanismo: W3P pause) - `set_publish_period(ms: 'int') -> 'None'` [verified] [Snapshot publicado] Periodo de publicación del estado del mundo (16~1000 milisegundos; por defecto 50). Cada recopilación cuesta unos 0.5 ms, así que 33 ms no es problema; el valor es único para toda la máquina y gana el último que se escribe. (Mecanismo: bloque del mundo requestedPeriodMs) - `say(u, text: 'str', seconds: 'float' = 4.0) -> 'bool'` [verified] [Canal de control] Muestra un bocadillo de chat sobre la unidad (para retransmisiones / depuración; no afecta al juego). Devuelve False si el bocadillo no apareció; el motivo queda en g.last_say_error. (Mecanismo: acción 56) - `message(text: 'str') -> 'bool'` [inferred] [Canal de control] Escribe una línea en la zona de mensajes de la esquina inferior izquierda del juego (solo se ve en esta máquina). Hay que esperar a que el juego muestre antes un aviso propio (la DLL captura el cuadro de mensajes a partir de ese aviso). (Mecanismo: acción 45) - `end_game() -> 'bool'` [verified] [Canal de control] Termina este proceso del juego (farm.py --keep abre automáticamente la siguiente partida según next_game.json). (Mecanismo: acción 22) - `canvas()` [verified] [Escritura directa] Lienzo: dibuja sobre la pantalla del juego cuadros de texto, paneles, barras de progreso, imágenes, círculos en el suelo y rutas (openwar3.canvas.Canvas). Lo dibuja el propio runtime, sin crear handles del juego ni cambiar su estado: es seguro también en multijugador; estilo libre (chino, esquinas redondeadas, transparencia). (Mecanismo: W3P 73 canvas_enable + memoria compartida Local\War3Canvas_ (el runtime lo dibuja en cada fotograma justo antes de que el juego dibuje el puntero del ratón; el puntero queda por encima)) - `press_to_continue() -> 'bool'` [verified] [Escritura directa] Pulsa una vez la barra espaciadora en la pantalla de carga de "Pulsa cualquier tecla para continuar". Muchos mapas RPG / de campaña necesitan una tecla al terminar de cargar para empezar (medido el 09-24 con WarChasers: sin pulsarla se queda en la pantalla de carga, con el reloj de juego a 0 y el carril rápido sin vaciarse). openwar3.run la pulsa solo mientras espera a entrar en la partida; normalmente no hace falta llamarla a mano. (Mecanismo: PostMessage WM_KEYDOWN/UP de la barra espaciadora a la ventana del juego (sin robar el foco)) ## Sandbox Canal JASS: crear unidades, fijar aliados, cambiar nombres, mostrar texto… para asistentes de RPG y compañeros; solo puede cambiar el mundo en partidas de un jugador y en herramientas locales. - `jass()` [verified] [Carril rápido] Llama a cualquier native de JASS por su nombre: g.jass.CreateUnit(g.jass.Player(1), "Hpal", x, y, 270.0). Los parámetros I/R/B/S/H se convierten solos (las unidades y objetos se pasan tal cual); en multijugador solo se pueden llamar las de solo lectura. Detalles en openwar3/jass.py y docs/COMPANION_ZH.md. (Mecanismo: W3P 70 jass (el runtime busca la native por nombre en su tabla de 1291)) - `player_slots() -> 'list[dict]'` [verified] [Carril rápido] Los 16 slots de jugador: controller (user = persona real / computer / neutral…), state (empty / playing / left), human, me, ally (si es aliado mío). En mapas RPG sirve para buscar un slot libre donde poner al compañero y para saber si la partida es de un jugador. (Mecanismo: JASS GetPlayerController / GetPlayerSlotState / IsPlayerAlly) - `spawn(code: 'str', x: 'float', y: 'float', player: 'int | None' = None, facing: 'float' = 270.0)` [verified] [Carril rápido] Crea una unidad en (x,y) (player es por defecto el jugador local) y devuelve la unidad de la instantánea (espera a la siguiente publicación del mundo, ~50 ms); si no se puede crear, devuelve None. La unidad devuelta tiene un atributo más, jass_handle. ⚠ Solo funciona en partidas de un jugador (en multijugador se desincroniza). (Mecanismo: JASS CreateUnit + W3P 72 handle -> unidad) - `set_alliance(a: 'int', b: 'int', allied: 'bool' = True, vision: 'bool' = True, control: 'bool' = False, xp: 'bool' = False, both: 'bool' = True) -> 'None'` [verified] [Carril rápido] Fija la relación de alianza del jugador a hacia b: allied = no atacarse + pedirse ayuda; vision = visión compartida; control = control compartido de unidades (b puede mandar las unidades de a); xp = experiencia compartida. both=True fija los dos sentidos a la vez (control solo se fija de a -> b). (Mecanismo: JASS SetPlayerAlliance) - `set_player_name(player: 'int', name: 'str') -> 'None'` [verified] [Carril rápido] Cambia el nombre del jugador (el que aparece en el marcador, el chat y el panel de aliados). Sirve para ponerle nombre al compañero. (Mecanismo: JASS SetPlayerName) - `show_text(text: 'str', seconds: 'float' = 6.0, player: 'int | None' = None) -> 'None'` [verified] [Carril rápido] Muestra una línea de texto en la esquina inferior izquierda de la pantalla (el tipo de texto que usan los disparadores del mapa); por defecto, para el jugador local. Admite códigos de color |cffRRGGBB. (Mecanismo: JASS DisplayTimedTextToPlayer) ## Conexión y utilidades Estado de la conexión y utilidades de cálculo puro. - `status() -> 'dict'` [verified] [Cálculo local] Estado de la conexión: pid, publicación del mundo (periodo, tiempo de recopilación), contadores del carril rápido. (Mecanismo: bloque del mundo + carril rápido + tabla de reservas) - `nearest(candidates, to)` [verified] [Cálculo local] El candidato más cercano a to (una unidad o (x,y)); sin candidatos devuelve None. (Mecanismo: cálculo puro)