본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 30. 17:30

컴퓨터에게 4X 게임 플레이 가르치기: Annhexation AI의 작동 방식

요약

4X 전략 게임을 위한 Annhexation AI의 설계 구조를 설명합니다. 전략, 운영, 전술의 세 계층을 분리하여 게임 플레이의 일관성을 확보하고 플레이어에게 도전적인 경험을 제공하는 방법을 다룹니다.

핵심 포인트

  • 전략, 운영, 전술 계층의 의도적 분리를 통한 일관성 확보
  • 수십 턴 동안 지속되는 장기적 목표 설정 메커니즘
  • 이벤트 감지를 통한 유연한 인터럽트 처리
  • 단일 목표에 집중하는 끈기 있는 AI 설계

4X 전략 게임을 위한 믿을 만한 컴퓨터 상대(Opponent)를 구축하는 것은 끝이 없는 문제 중 하나입니다. 겉보기에는 단순해 보인다는 상투적인 표현을 쓰고 싶지만... 사실 그렇지 않다고 생각합니다. 저는 처음부터 이것이 매우 어려운 과제(tough nut)가 될 것이라고 생각했습니다. 저는 이전에 체스 엔진을 구축한 적이 있는데, 강력한 상대를 만드는 데 있어 그것이 훨씬 더 간단했습니다. 물론 체스는 매우 잘 이해되어 있고 문서화가 잘 된 문제라는 점이 도움이 되긴 합니다. 플레이어는 명확한 의도를 가지고 *탐험(Explore), 확장(Expand), 착취(Exploit), 말살(Exterminate)*하는 상대를 원합니다. 즉, 여러 턴에 걸쳐 군대를 소집하고, 대륙을 가로질러 행군시키고, 당신의 해안에 상륙하여 도시를 점령하는, 당신이 다가오는 것을 지켜보면서도 완전히 막을 수 없는 상대를 원합니다. 플레이어는 유닛을 순간이동시키거나, 마음을 읽거나, 당신이 사거리 안으로 들어올 때까지 시작 도시에서 무기력하게 앉아 있는 상대를 원하지 않습니다.

이 포스트는 Annhexation AI를 둘러보는 투어로, AI가 어떻게 결정을 내리는지, 턴 사이에 무엇을 기억하는지, 그리고 어떻게 동일한 핵심 메커니즘이 8개의 서로 다른 문명과 4개의 난이도를 만들어내는지 설명합니다. Annhexation은 오픈 소스(Open source)가 아니므로, 구현 내용을 그대로 인용하기보다는 설계를 설명하고 흥미로운 부분들을 의사코드(Pseudocode)로 보여드리겠습니다.

이 AI는 아직 개발 중이지만, 수많은 시행착오(bashing with a hammer)를 거친 끝에 꽤 괜찮은 수준에 도달했다고 느낍니다.

핵심 아이디어

Annhexation AI에서 가장 중요한 단일 설계 결정은 전략(Strategy), 계획(Planning), 실행(Execution)이 분리(Decoupled)되어 있다는 점입니다. 이들은 의도적으로 분리된 세 가지 계층이며, AI의 턴은 이 세 계층을 통해 흐릅니다.

  1. 전략 계층 (Strategic layer)내가 달성하려는 것은 무엇인가? 평화 혹은 전쟁, 확장 혹은 내실 다지기, 과학 경쟁 혹은 불가사의(Wonders)를 통한 수성(Turtle). 이 계층은 수십 턴 동안 지속되는 목표 단위로 사고합니다.
  2. 운영 계층 (Operational layer)어떻게 전략적 목표를 달성할 것인가? 자원 할당, 유닛 할당량, 공격 계획, 도시 생산, 연구 방향. 이것은 계획 단계입니다.
  3. 전술 / 실행 계층 (Tactical / execution layer)이번 턴에 실제로 무엇을 할 것인가? 이 유닛을 여기로 이동, 저 스택(Stack)을 공격, 이 주둔군을 요새화, 이 병력을 상륙. 턴 단위의 실행입니다.

이러한 분리의 보상은 희망적으로는 시간이 흐름에 따른 일관성(Coherence)입니다. 탐욕적인 턴 단위 AI는 불안정해 보입니다. 군대를 만들었다가, 주의가 분산되어 해산하고, 다시 다른 군대를 만드는 식입니다. 반면, militaryPush 목표를 채택한 Annhexation AI는 해당 목표를 20턴 이상 유지하며, 도시가 함락되거나, 캠페인이 명백히 실패하거나, 혹은 지각 변동과 같은 중단 요소가 발생할 때까지 생산, 연구, 유닛 이동을 단일 목표로 집중시킵니다. 전략은 끈기 있게 유지되어야(Sticky) 하는 반면, 실행은 유연해야 합니다.

전체 턴은 위협 평가와 외교부터 전투, 이동, 생산, 요새화에 이르기까지 별개의 단계들이 순서대로 나열된 시퀀스로 실행됩니다:

function runTurn(player, world, aiState):
    detectEvents(aiState, world)          # 지난 턴과의 차이점 감지 → 인터럽트(Interrupts) 발생
    aiState.goals = evaluateStrategy(player, world, aiState)
...

전략 (Strategy)

전략 계층의 핵심은 **우선순위가 지정된 목표 스택 (Prioritized goal stack)**입니다. 매 턴 AI는 현재의 목표를 유지하거나 이를 재평가하며, AI가 가질 수 있는 욕구의 목록은 매우 풍부합니다:

  • earlyExpand — 통합하기 전 N개의 도시를 건설
  • earlyRush — 공격적인 초반 공격으로 오프닝(opening) 활용
  • infrastructureConsolidation — 건물, 인구, 성장
  • militaryPush — 선택한 플레이어를 상대로 지속적인 전쟁 수행
  • defensiveWar / counterattack — 공격에 대응하거나 잃은 것을 탈환
  • navalInvasion — 멀리 떨어진 육지 점령
  • wonderRace, scienceVictoryPush, scoreOptimisation — 평화적인 승리 경로
  • raidWar, asymmetricWar — 정복 대신 경제적 괴롭힘 수행
  • warPreparation, nuclearFirstStrike, recovery — 상황별 특수 목표

목표(Goals)는 경직된 규칙에 따라 실행되는 것이 아니라, 서로 점수가 매겨지며 효용(utility)이 가장 높은 목표가 선택됩니다. 점수 산정에는 여러 신호가 혼합됩니다:

  • 근접성 (Proximity). 가장 가까운 적 도시가 얼마나 멀리 있는가? 멀리 떨어진 이웃(≥14 hexes)은 AI를 평화적인 확장으로 유도하며, 가까운 이웃(≤4 hexes)은 군사적 목표로 끌어당깁니다. 지형이 성향을 형성합니다.
  • 전력 균형 (Force balance). 시뮬레이션된 전투에서 승리하고 있는가? 교전에서 패배하면 군사적 목표는 억제되고 방어적 목표는 부풀려집니다.
  • 추격 (Catch-up). 도시 수에서 뒤처지면 확장 점수가 높아져, 포위된 AI가 성장에 더 집중하게 됩니다.
  • 기회 (Opportunity). 현재 조우한(met) 모든 라이벌이 어떻게 행동하고 있는지에서 도출된 승수(multipliers) (이에 대해서는 아래에서 자세히 다룹니다).

그 후 모든 점수는 **성격 가중치 (personality weight)**에 의해 곱해집니다. 대략적으로 다음과 같습니다:

function scoreGoals(player, world, personality):
    scores = {}
    for goal in CANDIDATE_GOALS:
...

이 용어들 중 두 개는 세계(world)에 관한 것이고, 하나는 이 문명(civ)이 누구인지에 관한 것입니다. 이를 통해 동일한 평가 함수가 신중한 거북이와 날뛰는 군단을 만들어냅니다.

최상위 목표(우선순위 0)가 턴을 주도합니다. 차순위 목표들은 그 뒤에 대기하며, 인터럽트(interrupt)가 발생하는 즉시 역할을 이어받을 준비를 합니다.

상대방 읽기

자신의 제국만 바라보는 4X AI는 진공 상태에서 플레이하는 것과 같습니다. Annhexation의 AI는 누구와 싸울지 결정하기 전에, 조우한 모든 플레이어를 명시적으로 모델링합니다.

AI는 약 11개의 차원에 걸쳐 알려진 각 라이벌을 프로파일링하며, 각 차원은 [0, 1] 범위로 정규화(normalised)됩니다:

  • militarisation (군사화), development (발전), expansionism (팽창주의), techPace (기술 속도)
  • exposure (노출도) 및 coastalExposure (해안 노출도) (방어되지 않았거나 주둔군이 약한 도시)
  • borderTension (국경 긴장도) 및 aggression (공격성) (우리 국경 근처에 병력을 집결시키거나 현재 전쟁 중인 상태)
  • wonderFocus (경이 집중도), scienceFocus (과학 집중도), 그리고 매우 중요한 isRunawayLeader (독주 중인 리더 여부) 플래그

또한 AI는 지난 5턴 동안의 추세(상승, 정체 또는 하락)를 추적하므로, 단순히 현재 강한 라이벌이 아니라 '가속화되고 있는' 라이벌에게 반응합니다. 이러한 스냅샷(snapshots)은 영구적인 상태(persistent state)로 유지되어, 턴이 바뀌어도 추세 탐지(trend detection)가 유지됩니다.

두 번째 단계에서는 이러한 프로파일을 전쟁 목표 순위(war-target ranking)로 변환합니다. 각 라이벌에 대해 다음 요소들을 가중치로 계산합니다:

  • 공격성 친화도 (Aggression affinity) — 이 플레이어를 공격하는 것이 나의 성격에 부합하는가?
  • 강함 (Strength) — 내가 실제로 승리할 수 있는가?
  • 접근성 (Accessibility) — 내가 그들에게 도달할 수 있는가?
  • 안정성 (Stability) — 그들이 다른 전쟁으로 인해 편리하게 주의가 분산되어 있는가?
function scoreWarTargets(rivals, me, personality):
    for r in rivals:
        affinity      = personality.aggression × r.borderTension
...

이 점수 산정의 승자가 militaryPush (군사적 압박)의 목표가 되며, 그 규모는 목표 평가(goal evaluation) 시 기회 승수(opportunity multiplier)로 피드백됩니다. 노출되어 있고, 접근 가능하며, 주의가 분산된 이웃은 AI가 인지하고 이용하도록 설계된 유혹의 대상입니다.

성격(Personalities)과 교리(doctrine)

Annhexation에서의 성격은 단일한 "공격성" 슬라이더가 아닙니다. 이는 약 20개의 가중치 벡터(군사 생산, 공격 욕구, 확장, 경이 건설, 연구, 해군 생산, 약탈 선호도, 그리고 두 번째 도시의 긴급성 및 첫 번째 건설 선호도와 같은 초반 게임 튜닝)로 구성됩니다.

그 위에 교리(doctrine) 시스템이 자리 잡고 있습니다. 이는 해당 가중치와 AI의 유닛 구성 선호도를 무시(override)하는 8가지 문명별 플레이북(playbooks)입니다.

문명 (Civ)교리 (Doctrine)특징 (Signature)
몽골 (Mongolia)HORSE_RUSH군사 생산량 +50%, 공격력 +50%, 약탈 선호도 2배, 기병 중심 군대
.........

교리 (doctrine)는 공유된 메커니즘만을 조절하기 때문에, 이집트 (Egypt)와 몽골 (Mongolia)은 동일한 목표 평가 (goal-evaluation) 및 전투 코드를 실행합니다. 단지 그 가중치를 완전히 다른 목적에 맞게 조정할 뿐입니다. 몽골은 기병으로 당신을 압도하고, 이집트는 불가사의 (wonders)와 문화 (culture) 뒤에 숨으며, 영국 (England)은 해안선을 위해 싸웁니다.

각 문명 고유의 유닛들과 결합되어, 이는 각 문명에 독특한 개성을 부여합니다.

작전 계획: 의도에서 명령으로 (Operational planning: from intent to orders)

목표가 선택되면, 작전 계층 (operational layer)은 의도를 구체적인 계획으로 전환합니다.

유닛 할당량 (Unit quotas)은 개척자 (settlers), 노동자 (workers), 주둔군 (garrison), 야전군 (field army), 예비군 (reserve), 해군 (naval), 약탈자 (raiders) 등 각 유닛 클래스에 대한 제국 전역의 수요를 계산하며, 각 수요는 목표, 위협 수준, 개성 및 난이도에 따라 조정됩니다. 예를 들어, 성벽이 있는 도시에 대한 militaryPush (군사적 압박) 중에는 위협 수준에 따라 주둔군 할당량이 상승하고, 근접 유닛 (melee) 수요가 급증하며, 공성 유닛 (siege units)이 필수 사항이 됩니다. 공성 유닛 없이는 성벽을 허물 수 없으며, AI는 이 사실을 알고 있습니다.

유닛 구성 (Unit composition)은 군대의 근접/원거리/공성/기병 비율을 결정합니다. 성벽이 없는 도시에 대해서는 원거리 유닛 (ranged units, 무료 피해)을 대거 배치하고, 성벽에 대해서는 반드시 공성 유닛을 가져와야 합니다. 교리 (doctrine)는 이 혼합 비율을 기울게 만들며, 자원 제한 (resource gating)이 이를 캡(cap)합니다. 말이 없으면 기병이 없고, 철이 없으면 공성 유닛이 없습니다. 그것으로 끝입니다.

function targetComposition(target, doctrine, resources):
    if target.walled: mix = {melee: 0.4, siege: 0.4, ranged: 0.2}
    else:             mix = {melee: 0.4, ranged: 0.5, mounted: 0.1}
...

공격 계획 (Attack plans)은 명시적인 생애주기 (lifecycle)를 가진 일급 객체 (first-class objects)이며, 여러 턴에 걸쳐 진행됩니다:

집결 (mustering) → 집결 (gathering) → 전진 (advancing) → 포위 (besieging) → 강습 (assaulting)
                ↘ (해군) 수송 대기 (awaitingTransport) → 승선 (embarking) → 항해 (sailing) → 상륙 (landing) ↗

대상 선택 (Target selection) 단계에서는 적 도시의 거리에 따라 점수를 매깁니다 (거리 1헥스(hex)당 -5점). 여기에 성벽이 없는 경우(+15점), 수도인 경우(+10점), 그리고 AI가 필요로 하는 철(iron)이나 말(horses) 근처에 위치한 경우(성격과 긴급도에 따라 제한되는 큰 배율 적용) 보너스가 부여됩니다. AI는 도달 가능한 가장 약한 목표를 먼저 노리며, 일단 결정하면 실행에 옮깁니다.

도시 생산은 분산된 우선순위 큐 (distributed priority queue) 방식으로 운영됩니다. 생산량이 높은 도시는 전역적인 군사적 필요를 우선적으로 충족하며, 생산량이 낮은 도시는 개척자(settlers)와 일꾼(workers)을 보충합니다. 우선순위 폭포 (priority cascade)는 활성화된 목표에 따라 업그레이드 → 개척자 → 주둔군 (garrison) → 육군 (military) → 해군 (naval) → 일꾼/도로 (workers/roads) → 건물 (buildings) → 불가사의 (wonders) 순으로 진행됩니다.

연구는 목표를 따릅니다. 확장 중인 AI는 바퀴 (wheel)와 축산 (animal husbandry) 기술을 직진하여 연구합니다. 과학 승리 (science-victory)를 노리는 AI는 로켓 공학 (rocketry)을 향해 하드코딩된 경로를 따라가며, 전쟁 중인 AI는 군사 기술의 가중치를 높입니다. AI는 선행 기술 트리 (prerequisite tree)를 탐색하지만, 3개 이상의 기술이 필요한 긴 경로는 포기합니다. 즉, 100턴씩 돌아가는 우회로는 허용하지 않습니다. 이론적으로는 말이죠!

일꾼 관리 (Worker management)는 도시와 전략 자원 사이의 도로 경로를 계획하고 캐싱하며, 국경이 바뀌면 해당 경로를 무효화합니다. 병목 현상 탐지 (Bottleneck detection)는 군사 현대화가 지연되는 _이유_를 명시적으로 진단합니다. 기술을 기다리는 중인지, 철로 가는 도로 접근성이 부족한지, 무역을 위한 통화 (currency)가 부족한지 등을 파악하며, 병목 현상이 지속될수록 긴급도를 높입니다.

전술적 실행: 턴별 단계별 진행

계획이 완료되면, AI는 턴을 정해진 순서에 따른 단계별 시퀀스로 실행합니다. 대략적인 순서는 다음과 같습니다:

이벤트 탐지 및 도시 상실 대응      (지난 턴의 스냅샷과 비교)
비상 주둔군 충원                  (적군이 도시 타일에 위치한 경우)
유닛 업그레이드 및 소환 (recalls)
...

몇 가지 요소는 더 자세히 살펴볼 가치가 있습니다.

  • 전투 시뮬레이션 (Combat simulation)은 공격을 실행하기 전 각 공격을 예측합니다: 공격력 (난이도에 따른 효율성 승수(effectiveness multiplier)로 조정됨) 대 방어력 (주둔군(garrison), 지형(terrain) 및 요새화(fortify) 보너스)을 비교하여, 승리 확률과 예상 HP 손실로 변환합니다.
  • 더 높은 난이도에서는 **전투 단계 (combat phasing)**가 원거리 공격 우선, 근접 반격, 근접 마무리 순으로 모델링됩니다. 따라서 AI는 근접 유닛이 투입되기 전 궁수로 목표를 약화시키는 것의 가치를 이해합니다. Easy 난이도에서는 이 단계가 비활성화되어 의도적으로 AI의 지능을 낮춥니다.
function shouldAttack(attacker, defender, difficulty):
    atk = attacker.strength × difficulty.combatEffectiveness
    def = defender.strength × terrainBonus × fortifyBonus × garrisonBonus
...
  • 이동 (Movement)은 모든 유닛 간에 컨텍스트를 공유하므로 두 유닛이 결코 동일한 타일에 계획을 세우지 않습니다 (의도치 않은 중첩 방지). 이는 A* 알고리즘을 보조로 사용하는 전략적 경로 탐색 (strategic pathing)과 반진동 (anti-oscillation) 규칙을 사용합니다. 즉, 유닛이 부상을 입거나 인접한 적이 없는 한 지난 몇 턴 동안 점유했던 타일로 다시 돌아가지 않으며, 이는 전형적인 "AI 유닛이 영원히 앞뒤로 떨리는" 버그를 제거합니다.

  • 후퇴 (Retreat)는 HP 임계값 미만인 유닛(Easy에서는 50%, Deity에서는 20%까지)이나 주변 유닛 비율이 2:1로 열세인 경우에 발생합니다. 하지만 주둔군(garrison)은 절대 후퇴하지 않으며, 공격에 투입된 유닛은 15% 미만일 때만 퇴각하고, 병력을 실은 수송선(transports)은 절대 도망치지 않습니다. 투입된 결의(Commitment)가 존중됩니다.

  • 도시 방어 사령관 (City Defence Commander)은 자체적인 작은 상태 머신 (state machine) — reinforcing(강화) → defending(방어) → critical(위기) → secure(안전) — 을 통해 위협받는 각 도시의 주둔군을 자동화하며, 지역 전력 균형을 추적하고 방어군에게 이동 명령을 내립니다. 도시는 전략 레이어(strategic layer)가 모든 헥스(hex)를 마이크로매니징하지 않아도 지능적으로 스스로를 방어합니다.

메모리 (Memory): AI가 턴 사이에 유지하는 것

이러한 다중 턴 일관성 (multi-turn coherence)은 지속성 (persistence) 없이는 작동하지 않습니다. AI의 상태 객체 (state object)는 턴 사이에 직렬화 (serialised)되며, 다른 것들과 함께 다음을 포함합니다:

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0