Mentales Modell
Snapshot, Befehl, Quittung, Event, Tick, Batch. Wer diese sechs Konzepte versteht, versteht, warum die API so aussieht und wie man schnellen Code schreibt.
Snapshot: lesen, ohne Wartezeit
Die Runtime erfasst alle 50 ms im Spiel-Thread die gesamte Welt und schreibt sie ins Shared Memory. g.snapshot() liefert dir eine vollständige, in sich konsistente Welt:
- 16 Spieler-Slots: Gold, Holz, Nahrung, Nahrungslimit, insgesamt gesammelte Menge, Volk;
- bis zu 1024 Einheiten: Typ, Besitzer, Koordinaten, TP / Mana (mit Maximum), aktuelle Order und Order-Ziel, was sie tatsächlich angreift (Aufgabenziel), Heldenstufe / EP / Fertigkeitspunkte, Sichtbarkeit für jeden Spieler;
- bis zu 256 Einheitendetails: 12 Fähigkeiten (Stufe, verbleibende Abklingzeit), 8 Buffs, 6 Inventarplätze;
- Gegenstände am Boden, Bäume (alle 2 Sekunden aktualisiert), Produktionstabelle (Fortschritt von Training / Forschung / Bau / Aufwertung), Spieluhr, Tageszeit im Spiel.
Eine Kopie zu lesen dauert etwa 0.4 ms (Parsing in Python), ohne auf den Spiel-Thread zu warten. Deshalb gilt: Lies, so oft du willst. Schnittstellen wie g.units(), g.my_army(), g.cooldown() und g.inventory() lesen alle aus demselben Snapshot – egal wie oft du sie in einem Tick aufrufst, es kostet kaum etwas.
Die Veröffentlichungsperiode ist einstellbar: g.set_publish_period(ms), 16 ~ 1000 Millisekunden. Eine Erfassung kostet im Spiel-Thread etwa 0.5 ~ 0.9 ms, auch 33 ms sind also kein Problem. Der Wert gilt für den ganzen Rechner; wer zuletzt schreibt, gewinnt.
Befehl: schreiben, etwa ein Frame
g.move / attack / gather / build / train / cast … werden vom Spiel-Thread ausgeführt. Die Runtime arbeitet die von Clients eingereichten Befehle gebündelt im Event-Dispatch des Spiel-Threads ab, ein Befehl wartet also etwa ein Frame (fällt er in einen Event-Cluster, ca. 0.1 ms, sonst bis zum nächsten Dispatch).
- Befehle nehmen eine Einheit oder eine Liste entgegen; alle Einheiten einer Liste bekommen den Befehl im selben Frame;
- Mit
queue='after'stellst du wie mit Shift an: erst die aktuelle Aufgabe erledigen, dann die neue; - Für die Einheiten im Befehl reichen die Objekte aus dem Snapshot; das SDK prüft ihre Identität über das Handle-Paar (Adressen werden von neuen Einheiten wiederverwendet, Handles nicht).
Quittung: für jeden Befehl
r = g.build(worker, "hbar", x, y)
if r: # die Engine hat angenommen
...
else:
r.reason # 'rejected(金不够)' (= nicht genug Gold)
r.verdict # 8
r.exec_us # wie viele Mikrosekunden dieser Befehl im Spiel-Thread lief
Die Quittung wird im selben Frame zurückgelesen: Order der Einheit vor und nach dem Befehl, Rückgabewert der Engine-Funktion, Grundcode der Machbarkeitsprüfung. Sie beantwortet, „ob die Engine den Befehl angenommen hat und warum nicht“, aber nicht, „ob er am Ende erfolgreich war“ – das zeigen Snapshot und Events.
Alle Statuscodes und Grundcodes findest du unter Quittungen und Grundcodes.
Event: was passiert ist
on_event(g, ev) übergibt dir vor jedem on_tick alle Events seit dem letzten Tick, eines nach dem anderen:
| Event | Bedeutung |
|---|---|
unit.appeared / unit.died / unit.removed | Einheit erscheint, stirbt, verschwindet (in eine Goldmine gehen, umgewandelt werden, Verwesen der Leiche zählen auch als Verschwinden – das heißt nicht, dass sie gestorben ist) |
unit.damaged / order.changed / owner.changed | TP-Verlust, neue Order, neuer Besitzer |
hero.levelup | Held steigt eine Stufe auf |
item.appeared / item.removed | Gegenstand am Boden erscheint, wird aufgehoben oder verbraucht |
damage | Engine-Ebene: jeder einzelne Treffer. Quelleinheit, Angriffstyp, Schadenstyp, tatsächlicher TP-Verlust, Schaden vor Rüstung |
killed | Engine-Ebene: Dieser Treffer hat sie getötet, mit Killer |
production.done | Training / Forschung / Bau / Aufwertung fertig, mit Vier-Zeichen-Code und benötigten Spielsekunden. Auch für Gegner |
spell.cast | Eine Einheit hat eine Fähigkeit gewirkt: Vier-Zeichen-Code der Fähigkeit, Stufe, Abklingzeit in Sekunden, Zielpunkt |
message | In einem Nachrichtenfeld auf dem Bildschirm erscheint eine Zeile: Spielhinweis (etwa, dass mehr Farmen nötig sind), Chat (.chat enthält Sprecher und Text), Systemnachricht |
selection.changed / player.left | Die Auswahl des lokalen Spielers hat sich geändert / ein Spieler hat das Spiel verlassen oder wurde als besiegt entfernt |
game.started / game.ended | Eine neue Partie beginnt / die Partie wurde verlassen |
Eingabe-Events wie Klicks auf Canvas-Buttons, Hotkeys und Klicks auf den Boden findest du unter Oberfläche & Eingabe.
Der Event-Stream ist global: Fertige Produktionen des Gegners und getötete Creeps sind ebenfalls enthalten. Filtere nach ev.owner oder nach dem Handle der Einheit.
Tick: der Takt des Bots
on_tick wird standardmäßig 5-mal pro Sekunde aufgerufen (Echtzeit). Die Dauer eines Ticks ist im Wesentlichen deine eigene Rechenzeit: Snapshots ohne Wartezeit, Befehle etwa ein Frame. Überschreitet ein Tick die Periode, verschiebt sich der nächste automatisch, statt dass sich Ticks aufstauen.
- Bei doppelter Geschwindigkeit nicht nach Echtzeit warten. Wenn du 3 Spielsekunden warten willst, prüf, ob
g.clock()um 3 gestiegen ist – keinsleep(1.5). - Kein
sleepinon_tick. Soll etwas „etwas später“ passieren, merk dir die aktuelle Spielzeit und prüf im nächsten Tick erneut.
Batch: Dutzende Befehle, nur einmal warten
Wenn du in einem Tick viele Befehle erteilst, pack sie in 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")
# am Ende des Blocks wird der ganze Batch eingereicht: im selben Frame ausgeführt, nur einmal auf den Spiel-Thread warten
- Befehle im Block liefern
Pending, das nach dem Block zur Quittung wird; wer es vor dem Ende des Blocks liest, bekommt eine Exception; - Wird im Block eine Exception ausgelöst, verfällt der ganze Batch (halb ausgeführte Befehle sind gefährlicher als gar keine);
- Gemessen mit 8 Bewegungen: einzeln 68 ~ 99 ms, als Batch 6.5 ~ 10 ms.
Dasselbe Prinzip gilt für Abfragen: g.can_do_many([(u, code), ...]) und g.tech_many([...]) fragen viele Dinge auf einmal ab.
Das eben Geschriebene lesen
Innerhalb desselben Ticks sieht der Snapshot deine gerade erteilten Befehle noch nicht (erst die nächste Veröffentlichung holt sie ein). Zwei Logikteile können sich so um denselben Arbeiter streiten: Der eine hat ihn gerade zum Bau eines Bauernhofs geschickt, der andere hält ihn laut Snapshot noch für untätig.
g.order_of(u) löst das: Bis der Snapshot aufgeholt hat, gilt die neue Order aus der Quittung. Ob eine Einheit untätig ist, prüfst du mit g.order_of(u), nicht mit u.order. g.idle_workers() schließt Arbeiter, die in diesem Tick gerade eine Aufgabe bekommen haben, bereits aus.
Latenzstufen
| Stufe | Kanal | Latenz | Verwendet für |
|---|---|---|---|
| 0 | Push-Snapshot + Event-Stream | Eine Kopie lesen ca. 0.4 ms; alle 50 ms neue Daten | Alle „lesenden“ Schnittstellen |
| 1 | Schnellspur | Ca. 1 Frame; bei 6 parallelen Prozessen Median 0.06 ms | Alle Befehle und Abfragen (Standard im SDK) |
| 2 | Steuerkanal | 20 ~ 40 ms | Fallback, einige UI-Aktionen (Spielgeschwindigkeit, Sprechblasen, Nachrichten) |
| 3 | Gateway (WebSocket / JSON) | Stufe 1 + ca. 1 ms | Beliebige Sprachen, Browser, LLMs, Programme auf anderen Rechnern |
In der API-Referenz ist bei jeder Schnittstelle angegeben, über welche Stufe sie läuft.