最初の Bot
10 行の最小 Bot から始めて、農民の生産、人口の確保、兵の生産、ヒーロー、出撃を加え、最後にレシートの読み方を理解します。
Bot とは openwar3.Bot を継承したクラスのことです。必要なフックだけをオーバーライドすれば、「見る」と「やる」は g(Game)が担当します。
最小の Bot
from openwar3 import Bot
class MyBot(Bot):
def on_start(self, g): # 試合に入った後に 1 回呼ばれる
g.message("参戦します")
def on_tick(self, g): # 毎秒約 5 回
for w in g.idle_workers():
g.gather(w, g.nearest(g.gold_mines(), w))
python tools/play.py --bot my_bot.py
手の空いた農民が最寄りの金鉱へ向かいます。フックは 4 つです。
| フック | 呼ばれるタイミング |
|---|---|
on_start(g) | 試合に入った後、最初のティックの前に 1 回 |
on_tick(g) | 毎ティック(デフォルトで毎秒 5 回)。1 ティックが時間を超えると自動的に後ろにずれ、どんどん溜まっていくことはありません |
on_event(g, ev) | 毎ティックの on_tick の前に、前回のティック以降のイベントを 1 件ずつ渡します |
on_end(g, reason) | 試合が終わったとき(ゲームプロセスがなくなった / 自軍のユニットがいなくなった / 手動で停止した)に 1 回 |
on_tick で例外が発生しても試合全体は中断されません。ランナーがスタックトレースを表示し、次のティックから続行します。20 ティック連続でエラーになったときだけ停止します。
経済を加える:農民を作り、人口を確保する
from openwar3 import Bot
class Economy(Bot):
def on_tick(self, g):
res = g.resources() # 読み取れないときは 0 ではなく None
halls = g.my_buildings({"htow", "hkee", "hcas"})
if res is None or not halls:
return
home = halls[0]
# 1. 手の空いた農民はゴールドを採掘
for w in g.idle_workers():
mine = g.nearest(g.gold_mines(), w)
if mine:
g.gather(w, mine)
# 2. 農民を作る:キューには 1 つだけ入れる(いっぱいにするとゴールドがキューにロックされる)
if len(g.my_workers()) < 15 and not g.queue(home):
g.train(home, "hpea")
# 3. 人口がもうすぐ上限:建設中でない農民を見つけ、本拠地の近くに Farm を建てる
if res["food_cap"] - res["food_used"] <= 6:
builder = next((w for w in g.my_workers() if not g.is_constructing(w)), None)
if builder:
g.build_near(builder, "hhou", home.x, home.y)
注目すべき書き方が 3 つあります。
g.queue(home)が空のときだけ訓練する。 毎ティック訓練命令を出すと 7 スロットのキューが埋まり、ゴールドがロックされます(実測:本拠地のキューに農民が 4 人入り、300 ゴールドがロックされて、序盤が大きく遅れました)。- 座標を決め打ちせずに
build_nearを使う。 近いところから順に置ける場所を探し、ティックをまたいで結果を追跡します。ゴールドが足りないときは何もしません。決め打ちの座標は、ちょうど木立の中かもしれません。 - 建設中の農民は選ばない。 ヒューマンの Farm は建設に 35 秒かかり、途中でワーカーを別の場所へ送ると基礎の工事が止まります。
4 種族すべてで動く完全版は brains/examples/hello_bot.py です:1 鉱山 5 人、鉱山がいっぱいなら伐採、工事が止まった基礎の建設再開まで行います。
Barracks、ヒーロー、出撃を加える
from openwar3 import Bot
WAVE = 8
class Rush(Bot):
def on_start(self, g):
self.attacking = False
def on_tick(self, g):
halls = g.my_buildings({"htow", "hkee", "hcas"})
if not halls:
return
home = halls[0]
# ヒーロー:祭壇があってヒーローがいない -> まず復活を試み、復活できなければ訓練する(ヒーローは唯一なので、死んだ後に訓練し直そうとすると拒否される)
altars = g.my_buildings({"halt"})
if altars and not g.my_heroes():
if not g.revive(altars[0]):
g.train(altars[0], "Hpal")
for h in g.my_heroes():
info = g.hero_info(h)
if info and info["skill_points"]:
g.learn(h, "AHhb") # Holy Light
# Barracks から Footman を出し続ける(キューには 1 つだけ)
for b in g.my_buildings({"hbar"}):
if not g.queue(b):
g.train(b, "hfoo")
# 1 波分たまったら出撃し、壊滅したら本拠地に戻る
army = g.my_army()
if len(army) >= WAVE:
self.attacking = True
elif len(army) < WAVE // 2:
self.attacking = False
if self.attacking:
target = g.nearest([e for e in g.enemies() if g.is_building(e)], home)
if target:
idle = [u for u in army if not g.order_of(u)] # 手の空いたユニットにだけ命令する
g.attack_move(idle, target.x, target.y)
完全版は brains/examples/rush_bot.py を参照してください(hello_bot を継承し、Barracks / 祭壇がなければ建てます)。
レシートを読む
すべてのコマンドはレシートを返します。if r: が「エンジンが受理した」という意味で、受理されなかった場合は r.reason に理由が入っています。
r = g.train(barracks, "hfoo")
if not r:
print(r.reason) # rejected(人口不够)(= 人口不足)
print(r.verdict) # 3
よく見る理由コード:3 人口不足、8 ゴールド不足、9 木材不足、32 キューがいっぱい、183 前提条件が足りない、221 その項目がない / 建設中 / ヒーローがすでにいる、1001 ターゲットが見えない。全一覧は レシートと理由コード を参照してください。
受理 ≠ 成功
レシートが示すのは「エンジンがこのコマンドを受け付けた」ということだけです。木立の中の建設地点もエンジンはその場で受理し、ワーカーが到着してから失敗します。スキルは中断されることもあります。結果はスナップショットとイベントで確認してください:建物は build_near で建て(基礎が現れたかを追跡してくれます)、スキルは g.cooldown() がクールダウンに入ったかを見ます。