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.
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
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.
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.
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 usessleep(1.5). - No uses
sleepdentro deon_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()::
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 (WebSocket / JSON) | Nivel 1 + alrededor de 1 ms | Cualquier lenguaje, navegadores, LLM, programas en otra máquina |
En el catálogo de la API, cada interfaz indica qué nivel usa.