Docs Conceptos clave

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:

EventoSignificado
unit.appeared / unit.died / unit.removedUna 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.changedPierde vida, cambia de orden, cambia de dueño
hero.levelupUn héroe sube de nivel
item.appeared / item.removedUn objeto aparece en el suelo, o alguien lo recoge o lo usa
damageA 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
killedA nivel de motor: este golpe la mató; incluye al asesino
production.doneTerminó 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.castUna unidad lanzó una habilidad: código de cuatro caracteres de la habilidad, nivel, segundos de enfriamiento, punto de lanzamiento
messageAparece 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.leftCambió la selección del jugador local / un jugador se fue o fue eliminado por derrota
game.started / game.endedEmpieza 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 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()::

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

NivelCanalLatenciaPara qué
0Instantánea push + flujo de eventosLeer una copia, unos 0.4 ms; datos nuevos cada 50 msTodas las interfaces de «observar»
1Carril rápidoAlrededor de 1 frame; mediana de 0.06 ms con 6 procesos en paraleloTodos los comandos y consultas (por defecto en el SDK)
2Canal de control20 ~ 40 msRespaldo y unas pocas operaciones de interfaz (velocidad de juego, bocadillos, mensajes)
3Pasarela (WebSocket / JSON)Nivel 1 + alrededor de 1 msCualquier lenguaje, navegadores, LLM, programas en otra máquina

En el catálogo de la API, cada interfaz indica qué nivel usa.