Docs Conceitos centrais

Modelo mental

Snapshots, comandos, recibos, eventos, ticks e lotes. Entenda esses seis conceitos e você vai entender por que a API tem esse formato e como escrever código rápido.

Snapshots: leitura sem espera

A cada 50 ms, o runtime coleta o mundo inteiro na thread do jogo e grava tudo em memória compartilhada. O que g.snapshot() entrega é um mundo completo e consistente:

  • 16 slots de jogador: ouro, madeira, comida, limite de comida, total coletado, raça;
  • Até 1024 unidades: tipo, dono, coordenadas, vida / mana (com os máximos), ordem atual e alvo da ordem, o que ela está atacando de fato (alvo da tarefa), nível / XP / pontos de habilidade do herói e a visibilidade dela para cada jogador;
  • Até 256 detalhes de unidade: 12 habilidades (nível, recarga restante), 8 buffs, 6 espaços de inventário;
  • Itens no chão, árvores (atualizadas a cada 2 segundos), tabela de produção (progresso de treino / pesquisa / construção / melhoria), relógio do jogo, hora do dia no jogo.

Ler um snapshot leva cerca de 0.4 ms (parse em Python), sem esperar a thread do jogo. Então: leia à vontade. APIs como g.units(), g.my_army(), g.cooldown() e g.inventory() tiram tudo do mesmo snapshot, então chamá-las muitas vezes em um tick custa pouco.

O período de publicação é ajustável: g.set_publish_period(ms), de 16 a 1000 ms. Uma coleta leva cerca de 0.5 ~ 0.9 ms na thread do jogo, então até 33 ms funciona. Há um único valor compartilhado pela máquina inteira; vale a última escrita.

Comandos: escrita, cerca de um frame

g.move / attack / gather / build / train / cast … são entregues à thread do jogo para execução. O runtime executa em lotes os comandos enviados pelos clientes dentro do despacho de eventos da thread do jogo, então um comando espera cerca de um frame (cerca de 0.1 ms quando cai num grupo de eventos; caso contrário, espera o próximo despacho).

  • Comandos aceitam uma unidade ou uma lista; as unidades de uma lista recebem a ordem no mesmo frame;
  • Adicionar queue='after' equivale ao Shift: faz isto depois de terminar o que está fazendo;
  • Você pode passar os objetos de unidade que pegou do snapshot; o SDK confere a identidade pelo par de handles (endereços são reutilizados por unidades novas, handles não).

Recibos: todo comando tem um

r = g.build(worker, "hbar", x, y)
if r:                      # o engine aceitou
    ...
else:
    r.reason               # 'rejected(金不够)'  (= falta ouro)
    r.verdict              # 8
r.exec_us                  # quantos microssegundos este comando levou na thread do jogo

Os recibos são lidos no mesmo frame: a ordem da unidade antes e depois do comando, o valor de retorno da função do engine e o código de motivo da verificação de viabilidade. Um recibo responde “o engine aceitou este comando e, se não, por quê”, mas não responde “no fim deu certo?” — para isso, olhe os snapshots e os eventos.

Para todos os códigos de status e códigos de motivo, veja Recibos e códigos de motivo.

Eventos: o que aconteceu

on_event(g, ev) roda antes de cada on_tick e entrega a você, um por um, todos os eventos desde o último tick:

EventoSignificado
unit.appeared / unit.died / unit.removedUma unidade apareceu, morreu ou sumiu (entrar numa mina de ouro, ser convertida ou um cadáver se decompor também contam como sumir — não é o mesmo que morrer)
unit.damaged / order.changed / owner.changedPerdeu vida, mudou de ordem, mudou de dono
hero.levelupUm herói subiu de nível
item.appeared / item.removedUm item no chão apareceu, foi pego ou foi usado
damageNível de engine: cada golpe. Unidade de origem, tipo de ataque, tipo de dano, vida realmente perdida, dano antes da armadura
killedNível de engine: este golpe a matou, com quem matou
production.doneTreino / pesquisa / construção / melhoria concluído, com o código de quatro caracteres e os segundos de jogo gastos. Também emitido para os adversários
spell.castUma unidade lançou uma habilidade: código de quatro caracteres da habilidade, nível, recarga em segundos, ponto de lançamento
messageApareceu uma linha numa caixa de mensagens da tela: dicas do jogo (“Você precisa de mais fazendas”), chat (.chat traz quem falou e o conteúdo), mensagens do sistema
selection.changed / player.leftA seleção do jogador local mudou / um jogador saiu ou foi removido por derrota
game.started / game.endedUma nova partida começou / saída da partida

Eventos de entrada, como cliques em botões do canvas, teclas de atalho e cliques no chão, estão em Interface e entrada.

O fluxo de eventos é global: as produções concluídas dos adversários e as mortes de creeps estão todas nele. Filtre por ev.owner ou pelo handle da unidade.

Ticks: o ritmo do Bot

Por padrão, on_tick é chamado 5 vezes por segundo (relógio real). O custo de um tick é basicamente só a sua própria computação: snapshots não têm espera e comandos levam cerca de um frame. Se um tick passar do período, o próximo é adiado automaticamente — eles nunca se acumulam.

  • Na velocidade 2×, não espere pelo relógio real. Para esperar 3 segundos de jogo, observe g.clock() avançar 3 — não use sleep(1.5).
  • Não use sleep dentro do on_tick. Se precisar “fazer daqui a pouco”, anote o tempo de jogo atual e confira de novo no próximo tick.

Lotes: dezenas de comandos, uma espera

Quando um tick envia muitos comandos, coloque-os dentro de 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")
# o lote inteiro é enviado quando o bloco termina: executado no mesmo frame, esperando a thread do jogo uma única vez
  • Comandos dentro do bloco retornam Pending, que vira um recibo quando o bloco termina; lê-lo antes disso lança um erro;
  • Se uma exceção for lançada dentro do bloco, o lote inteiro é descartado (meio conjunto de comandos é mais perigoso do que nenhum);
  • Medido com 8 movimentos: 68 ~ 99 ms um por vez, 6.5 ~ 10 ms em lote.

A mesma ideia vale para consultas: g.can_do_many([(u, code), ...]) e g.tech_many([...]) perguntam sobre muitas coisas de uma vez.

Lendo o que você acabou de escrever

No mesmo tick, o snapshot ainda não reflete os comandos que você acabou de enviar (ele só alcança na próxima publicação). Duas partes da lógica podem acabar disputando o mesmo trabalhador: uma acabou de mandá-lo construir uma fazenda, enquanto a outra olha o snapshot e acha que ele ainda está ocioso.

g.order_of(u) resolve isso: até o snapshot alcançar, ele usa a nova ordem do recibo. Para decidir se uma unidade está ociosa, use g.order_of(u), não u.order. g.idle_workers() já exclui os trabalhadores que receberam uma tarefa neste tick.

Faixas de latência

FaixaCanalLatênciaUsado para
0Snapshot enviado + fluxo de eventosCerca de 0.4 ms por leitura; dados novos a cada 50 msTodas as APIs de “observação”
1Via rápidaCerca de 1 frame; mediana de 0.06 ms com 6 processos em paraleloTodos os comandos e consultas (padrão do SDK)
2Canal de controle20 ~ 40 msPlano B e algumas operações de interface (velocidade do jogo, balões de fala, mensagens)
3Gateway (WebSocket / JSON)Faixa 1 + cerca de 1 msQualquer linguagem, navegadores, LLMs, programas em outra máquina

Cada API na referência da API indica qual faixa ela usa.