첫 번째 Bot
10줄짜리 최소 Bot에서 시작해 농부 생산, 인구 확보, 병력 생산, 영웅, 공격을 추가하고 마지막으로 회신을 읽는 법을 익힙니다.
Bot은 openwar3.Bot을 상속한 클래스입니다. 필요한 훅만 오버라이드하면 되고, g(Game)가 “보기”와 “하기”를 맡습니다.
최소 Bot
from openwar3 import Bot
class MyBot(Bot):
def on_start(self, g): # 게임에 들어간 뒤 한 번 호출
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
놀고 있는 농부가 가장 가까운 금광으로 갑니다. 훅은 네 가지입니다:
| 훅 | 호출 시점 |
|---|---|
on_start(g) | 게임에 들어간 뒤, 첫 틱 전에 한 번 |
on_tick(g) | 매 틱(기본 초당 5번). 한 틱이 시간을 넘기면 자동으로 뒤로 밀리며, 밀린 틱이 쌓이지는 않습니다 |
on_event(g, ev) | 매 틱의 on_tick 직전에, 이전 틱 이후의 이벤트를 하나씩 전달 |
on_end(g, reason) | 이번 게임이 끝날 때(게임 프로세스 종료 / 아군 유닛 전멸 / 수동 중지) 한 번 |
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. 인구가 거의 찼으면: 건물을 짓고 있지 않은 농부를 골라 본진 건물 근처에 농장 건설
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)
눈여겨볼 작성법 세 가지:
g.queue(home)이 비어 있을 때만 훈련합니다. 매 틱 훈련 명령을 내리면 7칸 대기열이 가득 차고 돈이 묶입니다(실측: 본진 건물에 농부 4명이 대기하면서 금 300이 대기열에 묶여 초반이 크게 늦어졌습니다).- 좌표를 하드코딩하지 말고
build_near를 씁니다. 가까운 곳부터 지을 수 있는 지점을 스스로 찾고, 여러 틱에 걸쳐 결과를 추적하며, 돈이 부족하면 아무것도 하지 않습니다. 하드코딩한 좌표는 숲 한가운데일 가능성이 큽니다. - 건물을 짓고 있는 농부는 고르지 않습니다. 휴먼 농장은 짓는 데 35초가 걸리는데, 도중에 일꾼을 빼면 기초 공사가 멈춥니다.
네 종족 모두에서 동작하는 완전한 버전은 brains/examples/hello_bot.py입니다. 금광당 5명, 금광이 차면 벌목, 멈춘 기초 공사 이어서 짓기까지 들어 있습니다.
병영, 영웅, 공격 추가
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
# 병영은 계속 보병 생산(대기열에는 1개만)
for b in g.my_buildings({"hbar"}):
if not g.queue(b):
g.train(b, "hfoo")
# 한 웨이브가 모이면 공격, 많이 잃으면 귀환
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을 상속하며, 병영 / 제단이 없으면 짓습니다).
회신 읽기
모든 명령은 회신 하나를 반환합니다. 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()이 쿨다운에 들어갔는지 보세요.