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:
| Evento | Significado |
|---|---|
unit.appeared / unit.died / unit.removed | Uma 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.changed | Perdeu vida, mudou de ordem, mudou de dono |
hero.levelup | Um herói subiu de nível |
item.appeared / item.removed | Um item no chão apareceu, foi pego ou foi usado |
damage | Nível de engine: cada golpe. Unidade de origem, tipo de ataque, tipo de dano, vida realmente perdida, dano antes da armadura |
killed | Nível de engine: este golpe a matou, com quem matou |
production.done | Treino / 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.cast | Uma unidade lançou uma habilidade: código de quatro caracteres da habilidade, nível, recarga em segundos, ponto de lançamento |
message | Apareceu 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.left | A seleção do jogador local mudou / um jogador saiu ou foi removido por derrota |
game.started / game.ended | Uma 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 usesleep(1.5). - Não use
sleepdentro doon_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
| Faixa | Canal | Latência | Usado para |
|---|---|---|---|
| 0 | Snapshot enviado + fluxo de eventos | Cerca de 0.4 ms por leitura; dados novos a cada 50 ms | Todas as APIs de “observação” |
| 1 | Via rápida | Cerca de 1 frame; mediana de 0.06 ms com 6 processos em paralelo | Todos os comandos e consultas (padrão do SDK) |
| 2 | Canal de controle | 20 ~ 40 ms | Plano B e algumas operações de interface (velocidade do jogo, balões de fala, mensagens) |
| 3 | Gateway (WebSocket / JSON) | Faixa 1 + cerca de 1 ms | Qualquer linguagem, navegadores, LLMs, programas em outra máquina |
Cada API na referência da API indica qual faixa ela usa.