文件 核心概念

心智模型

快照、命令、回執、事件、一拍、批次。理解這六個概念,就理解了 API 為什麼長這樣,以及怎麼寫才快。

快照:讀取,零等待

執行環境每 50 ms 在遊戲執行緒上把整個世界採集一遍,寫進共享記憶體。g.snapshot() 拿到的是一份完整、自洽的世界:

  • 16 個玩家欄位:金、木、人口、人口上限、累計採集量、種族;
  • 最多 1024 個單位:類型、擁有者、座標、生命 / 魔力(含上限)、目前訂單和訂單目標、實際在打誰(任務目標)、英雄等級 / 經驗 / 技能點、每個玩家對它的可見性;
  • 最多 256 份單位細節:12 個技能(等級、剩餘冷卻)、8 個 buff、6 格物品欄;
  • 地上物品、樹(每 2 秒更新)、生產表(訓練 / 研究 / 建造 / 升級的進度)、遊戲時鐘、遊戲內時間。

讀一份約 0.4 ms(Python 解析),不必等遊戲執行緒。所以:儘管讀。g.units()、g.my_army()、g.cooldown()、g.inventory() 這類 API 都是從同一份快照裡取值,一拍裡呼叫多少次都不貴。

發布週期可以調整:g.set_publish_period(ms),16 ~ 1000 毫秒。一次採集在遊戲執行緒上約 0.5 ~ 0.9 ms,設成 33 ms 也沒問題。整台機器共用一個值,以最後寫入的為準。

命令:寫入,約一幀

g.move / attack / gather / build / train / cast … 交給遊戲執行緒執行。執行環境在遊戲執行緒的事件分派裡批次執行用戶端提交的命令,所以一條命令大約等一幀(落在事件叢集裡時約 0.1 ms,否則等到下一次分派)。

  • 命令可以傳入一個單位或一個清單,清單裡的單位在同一幀一起下令;
  • 加上 queue='after' 就是 Shift 排隊:做完手上這件再做;
  • 命令裡的單位直接用從快照拿到的物件即可,SDK 用控制代碼對核對身分(位址會被新單位重複使用,控制代碼不會)。

回執:每條命令都有

r = g.build(worker, "hbar", x, y)
if r:                      # 引擎接下了
    ...
else:
    r.reason               # 'rejected(金不够)'(= 金不夠)
    r.verdict              # 8
r.exec_us                  # 這一條在遊戲執行緒上執行了幾微秒

回執是在同一幀裡讀回來的:下令前後單位的訂單、引擎函式的回傳值、可行性檢查的原因碼。它回答「引擎有沒有接下這條命令、為什麼沒接下」,但不回答「最後做成了沒有」—— 做成了沒有要看快照和事件。

全部狀態碼和原因碼見 回執與原因碼。

事件:發生了什麼

on_event(g, ev) 會在每拍的 on_tick 之前,把上一拍以來的事件逐一交給你:

事件含義
unit.appeared / unit.died / unit.removed單位出現、死亡、消失(進入金礦、被轉化、屍體腐爛也算消失,不等於死亡)
unit.damaged / order.changed / owner.changed掉血、換訂單、換擁有者
hero.levelup英雄升級
item.appeared / item.removed地上的物品出現、被撿走或用掉
damage引擎層級:每一下傷害。來源單位、攻擊類型、傷害類型、實際扣血、護甲前傷害
killed引擎層級:這一下把它打死了,帶有兇手
production.done訓練 / 研究 / 建造 / 升級完成,帶有四字碼和花了多少遊戲秒。對手的也有
spell.cast單位放了技能:技能四字碼、等級、冷卻秒數、施法點
message螢幕訊息框裡出現一條:遊戲提示(「需要更多的農場」)、聊天(.chat 裡有發言者和內容)、系統訊息
selection.changed / player.left本機玩家的選取變了 / 有玩家離開或被判定落敗移除
game.started / game.ended新的一局開始 / 離開對局

畫板按鈕被點擊、熱鍵、點地面這些輸入事件,請見 介面與輸入。

事件流是全域的:對手的生產完成、野怪的死亡都在裡面。請用 ev.owner 或單位控制代碼過濾。

一拍:Bot 的節奏

on_tick 預設每秒呼叫 5 次(依實際時間)。一拍的耗時基本上就是你自己的運算:快照零等待,命令約一幀。一拍超過週期會自動順延,不會越積越多。

  • 2 倍速下別依實際時間等待。 想等 3 遊戲秒,就看 g.clock() 增加了 3,不要 sleep(1.5)。
  • 別在 on_tick 裡 sleep。 需要「過一會兒再做」,就記下目前的遊戲時間,下一拍再判斷。

批次:幾十條命令只等一次

一拍要下很多條命令時,包在 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")
# 區塊結束時整批提交:同一幀執行,只等一次遊戲執行緒
  • 區塊裡的命令回傳 Pending,區塊結束後變成回執;區塊結束前讀取它會拋出錯誤;
  • 區塊裡拋出例外時,整批作廢(只送出一半的命令比不送更危險);
  • 實測 8 條移動:逐條 68 ~ 99 ms,一批 6.5 ~ 10 ms。

同樣的思路也適用於查詢:g.can_do_many([(u, code), ...])、g.tech_many([...]) 一次查詢很多個。

讀取自己剛寫入的

同一拍裡,快照還看不到你剛下的命令(下一次發布才會跟上)。兩段邏輯可能會搶同一個工人:一段剛派它去蓋農場,另一段看快照以為它還閒著。

g.order_of(u) 解決這個問題:快照跟上之前,它以回執裡的新訂單為準。判斷「是否閒置」用 g.order_of(u),不要用 u.order。 g.idle_workers() 已經把「這一拍剛被派了工作的」排除掉了。

延遲檔位

檔位通道延遲用於
0推送快照 + 事件流讀一份約 0.4 ms;資料每 50 ms 更新一份所有「看」的 API
1快車道約 1 幀;6 個行程並行時中位數 0.06 ms所有命令和查詢(SDK 預設)
2控制通道20 ~ 40 ms備援、少數介面類操作(倍速、氣泡、訊息)
3閘道(WebSocket / JSON)檔位 1 + 約 1 ms任何語言、瀏覽器、LLM、另一台電腦上的程式

API 目錄 裡每個 API 都標明了它走哪一個檔位。