文件 讓 AI 寫 Bot

Agent 自主迭代

讓 Coding Agent 自己跑對局、讀結果、改程式碼、再跑:它需要一條能無人值守執行的命令、一份結構化的對局報告,以及一個明確的目標。

在 用 LLM 寫一個 Bot 裡,「跑一局 → 看現象 → 告訴模型」這一步由你來做。能執行命令的 Coding Agent(Claude Code、Codex、Cursor 的 Agent 模式等)可以把這一步也接手,形成閉環:

  改程式碼 ──► 跑一局(無人值守)──► 讀對局報告 ──► 找出最影響結果的一處 ──┐
    ▲                                                                     │
    └─────────────────────────────────────────────────────────────────────┘

要讓這個閉環真正收斂,Agent 需要三樣東西。

1. 一條無人值守的命令

python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
  • --minutes 讓這一局一定會結束(以實際時間的分鐘計),Agent 不會卡在一局裡;
  • --speed 200 用 2 倍速節省時間 —— 但 Bot 裡要依遊戲時鐘等待(g.clock()),別依實際時間 sleep;
  • --fair 讓它從第一天起就照擂台規則寫,只看得見視野內的東西;
  • 執行結束時終端機會印出結束原因,例如 我方没有单位了、到时间了;Bot 自己 print 的內容也在終端機裡。

視窗最小化時,遊戲模擬是停住的。讓 Agent 用預設的視窗模式啟動遊戲,並且別和你正在使用的實例撞號(--inst)。

2. 一份結構化的對局報告

終端機輸出是給人看的。給 Agent 看的應該是一份 JSON:發生了什麼、什麼沒做成、為什麼。SDK 已經把原料都給你了 —— 回執帶原因碼,事件流帶生產完成和傷亡。把它們收集起來即可:

import collections, json, time
from openwar3 import Bot

class Recorder(Bot):
    """幫 Bot 加上一份對局報告。繼承它,再在自己的 on_start / on_event 裡呼叫 super()。"""

    def on_start(self, g):
        self.rejects = collections.Counter()   # "train hfoo: rejected(人口不够)" -> 次數(= 人口不夠)
        self.timeline = []                     # [遊戲秒, 類別, 四字碼]:訓練 / 研究 / 建造 / 升級完成
        self.lost = collections.Counter()      # 我方死了什麼
        self.killed = collections.Counter()    # 我方打死了什麼

    def check(self, r, what):
        """包住命令,記錄被拒原因:self.check(g.train(b, "hfoo"), "train hfoo")"""
        if r is not None and not r:
            self.rejects[f"{what}: {r.reason}"] += 1
        return r

    def on_event(self, g, ev):
        me = g.me()
        if ev.kind == "production.done" and ev.owner == me:
            self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code])
        elif ev.kind == "unit.died":
            (self.lost if ev.owner == me else self.killed)[ev.type] += 1

    def on_end(self, g, reason):
        report = {"reason": reason, "timeline": self.timeline, "lost": self.lost,
                  "killed": self.killed, "rejects": self.rejects.most_common(10)}
        try:                                   # 遊戲可能已經結束,讀不到就算了
            report |= {"clock": g.clock(), "resources": g.resources(),
                       "army": len(g.my_army()), "workers": len(g.my_workers())}
        except Exception:
            pass
        with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f:
            json.dump(report, f, ensure_ascii=False, indent=1)

這份報告能回答的問題:

訊號從哪裡來能看出什麼
被拒最多的原因回執 reason / verdict人口一直卡住(3)、錢不夠卻一直下令(8 / 9)、攻擊迷霧裡的目標(1001)、英雄陣亡了還在訓練(221)
生產時間軸production.done 事件(帶有花了多少遊戲秒)第幾秒出第一個英雄、第幾秒升級主城、兵營有沒有一直在出兵;可以和職業選手的開局比較
雙方傷亡unit.died 事件是不是一直在送兵、英雄陣亡幾次、打野有沒有賺
結束原因on_end(g, reason)我方没有单位了(我方沒有單位了)= 輸了;到时间了(時間到了)= 還沒分出勝負
最終兵力和資源on_end 時讀一次快照錢存著沒花 = 生產跟不上;工人太少 = 經濟沒起來

以程式判定勝負是 對戰平台 的地基實驗之一,還在路線圖上。目前可以用 我方没有单位了(我方沒有單位了)判定輸,用「看得見的敵方建築全滅」近似判定贏。

3. 一個明確的目標和幾條約束

把下面這段交給 Agent,依你的目標修改:

目標:讓 brains/my_bot.py 在 Echo Isles 上穩定打贏「簡單」難度的電腦(人類對隨機種族)。

每一輪:
1. 執行 python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
2. 讀終端機輸出和最新的 run_*.json:結束原因、生產時間軸、被拒最多的原因、雙方傷亡
3. 找出最影響結果的「一個」問題,只改這一處;在程式碼註解裡寫下修改理由和依據的資料
4. 回到第 1 步。連續 3 局沒有進步就停下來,把報告和你的判斷告訴我

約束:
- 只能用 docs/api.json 裡的方法,不要編造 API
- 不要每拍對同一個單位重複下同一條命令;只對閒著的單位下令
- 保持 --fair(只用視野內看得見的敵人)
- 改程式碼前先執行 python tools/run_tests.py,確認沒有把範例改壞

讓閉環更快收斂的幾個習慣

  • 一次只改一處。 同時改三處,贏了不知道是哪一處起作用,輸了也不知道是哪一處改壞的。
  • 比較時要打夠局數。 同一個局面的隨機性很大;兩局只看得出很大的差距。判斷「是否進步」至少要看幾局的趨勢。
  • 先修「被拒」再調策略。 回執裡被拒最多的原因,往往就是 Bot 最大的 bug。
  • 把判斷寫進註解。 下一輪的 Agent(或下一次對話)能從註解裡知道為什麼這樣寫,不會把修好的地方改回去。
  • 用離線測試兜底。 為關鍵邏輯寫不需要開遊戲的單元測試(範例 Bot 的測試在 brains/examples/tests/),Agent 每次改完先跑一遍。