Skip to main content

nil_core/behavior/
build.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::behavior::idle::IdleBehavior;
5use crate::behavior::{Behavior, BehaviorProcessor, BehaviorScore};
6use crate::continent::Coord;
7use crate::error::Result;
8use crate::ethic::EthicPowerAxis;
9use crate::infrastructure::building::StorageId;
10use crate::infrastructure::building::prefecture::build_queue::{
11  PrefectureBuildOrderKind,
12  PrefectureBuildOrderRequest,
13};
14use crate::infrastructure::prelude::*;
15use crate::infrastructure::queue::InfrastructureQueue;
16use crate::military::maneuver::Maneuver;
17use crate::world::World;
18use bon::Builder;
19use nil_util::iter::IterExt;
20use rand::random_range;
21use serde::{Deserialize, Serialize};
22use std::fmt::Debug;
23use std::marker::PhantomData;
24use std::ops::ControlFlow;
25use std::sync::LazyLock;
26use strum::IntoEnumIterator;
27
28pub(crate) static BUILD_TEMPLATE: LazyLock<Vec<BuildStep>> = LazyLock::new(generate_template);
29
30#[derive(Builder, Debug)]
31pub struct BuildBehavior {
32  coord: Coord,
33}
34
35impl BuildBehavior {
36  const MAX_IN_QUEUE: u8 = 3;
37}
38
39impl Behavior for BuildBehavior {
40  fn score(&self, world: &World) -> Result<BehaviorScore> {
41    let config = world.config();
42    let infrastructure = world.infrastructure(self.coord)?;
43    let max_in_queue = f64::from(Self::MAX_IN_QUEUE);
44
45    if let Some(in_queue) = infrastructure
46      .prefecture()
47      .turns_in_build_queue(&config)
48    {
49      Ok(BehaviorScore::new(1.0 - (in_queue / max_in_queue)))
50    } else {
51      Ok(BehaviorScore::MIN)
52    }
53  }
54
55  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
56    let mut behaviors = vec![IdleBehavior.boxed()];
57    macro_rules! push {
58      ($building:ident, $id:expr) => {{
59        let behavior = BuildBuildingBehavior::builder()
60          .marker(PhantomData::<$building>)
61          .coord(self.coord)
62          .building($id)
63          .build()
64          .boxed();
65
66        behaviors.push(behavior);
67      }};
68    }
69
70    for id in BuildingId::iter() {
71      match id {
72        BuildingId::Academy => push!(Academy, id),
73        BuildingId::Farm => push!(Farm, id),
74        BuildingId::IronMine => push!(IronMine, id),
75        BuildingId::Prefecture => push!(Prefecture, id),
76        BuildingId::Quarry => push!(Quarry, id),
77        BuildingId::Sawmill => push!(Sawmill, id),
78        BuildingId::Silo => push!(Silo, id),
79        BuildingId::Stable => push!(Stable, id),
80        BuildingId::Wall => push!(Wall, id),
81        BuildingId::Warehouse => push!(Warehouse, id),
82        BuildingId::Workshop => push!(Workshop, id),
83      }
84    }
85
86    BehaviorProcessor::new(world, behaviors)
87      .take(usize::from(Self::MAX_IN_QUEUE))
88      .try_each()?;
89
90    Ok(ControlFlow::Break(()))
91  }
92}
93
94#[derive(Builder, Debug)]
95pub struct BuildBuildingBehavior<T>
96where
97  T: Building + Debug,
98{
99  coord: Coord,
100  building: BuildingId,
101  marker: PhantomData<T>,
102}
103
104impl<T> BuildBuildingBehavior<T>
105where
106  T: Building + Debug,
107{
108  const STORAGE_CAPACITY_THRESHOLD: f64 = 0.8;
109}
110
111impl<T> Behavior for BuildBuildingBehavior<T>
112where
113  T: Building + Debug + 'static,
114{
115  #[allow(clippy::too_many_lines)]
116  fn score(&self, world: &World) -> Result<BehaviorScore> {
117    let infrastructure = world.infrastructure(self.coord)?;
118    let building = infrastructure.building(self.building);
119
120    if !building
121      .infrastructure_requirements()
122      .has_required_levels(infrastructure)
123    {
124      return Ok(BehaviorScore::MIN);
125    }
126
127    let level = infrastructure
128      .prefecture()
129      .resolve_level(self.building, building.level());
130
131    if level >= building.max_level() {
132      return Ok(BehaviorScore::MIN);
133    }
134
135    let stats = world.stats().infrastructure();
136    let owner = world.continent().owner_of(self.coord)?;
137    let ruler_ref = world.ruler(owner)?;
138
139    let required_resources = &stats
140      .building(self.building)?
141      .get(level + 1u8)?
142      .resources;
143
144    if !ruler_ref.has_resources(required_resources) {
145      return Ok(BehaviorScore::MIN);
146    }
147
148    // Prioritize the wall if there are incoming attacks.
149    // It will also check whether the build order can be
150    // completed before the nearest one arrives.
151    if let BuildingId::Wall = self.building
152      && let Some(distance) = world
153        .military()
154        .maneuvers()
155        .filter(|maneuver| maneuver.destination() == self.coord)
156        .filter(|maneuver| maneuver.is_attack() && maneuver.is_going())
157        .filter_map(Maneuver::pending_distance)
158        .min()
159    {
160      let workforce = stats
161        .building(self.building)?
162        .get(level + 1u8)
163        .map(|it| f64::from(it.workforce))?;
164
165      if workforce <= f64::from(distance) {
166        return Ok(BehaviorScore::MAX);
167      }
168    }
169
170    if let BuildingId::Farm = self.building
171      && !world
172        .get_maintenance_balance(owner.clone())?
173        .is_sustainable()
174    {
175      return Ok(BehaviorScore::MAX);
176    }
177
178    // Prioritize storage when its capacity is almost full.
179    if let Ok(id) = StorageId::try_from(self.building) {
180      let resources = ruler_ref.resources();
181      let capacity = world.get_storage_capacity(owner.clone())?;
182
183      let ratio = match id {
184        StorageId::Silo => f64::from(resources.food) / f64::from(capacity.silo),
185        StorageId::Warehouse => {
186          let capacity = f64::from(capacity.warehouse);
187          let iron_ratio = f64::from(resources.iron) / capacity;
188          let stone_ratio = f64::from(resources.stone) / capacity;
189          let wood_ratio = f64::from(resources.wood) / capacity;
190          iron_ratio.max(stone_ratio).max(wood_ratio)
191        }
192      };
193
194      if ratio >= Self::STORAGE_CAPACITY_THRESHOLD
195        && infrastructure
196          .prefecture()
197          .build_queue()
198          .iter()
199          .filter(|order| order.kind().is_construction())
200          .all(|order| order.building() != self.building)
201      {
202        return Ok(BehaviorScore::MAX);
203      }
204    }
205
206    let mut score = if BUILD_TEMPLATE
207      .iter()
208      .filter(|step| !step.is_done(infrastructure))
209      .take(3)
210      .any(|step| step.id == self.building)
211    {
212      BehaviorScore::new(random_range(0.8..=1.0))
213    } else {
214      BehaviorScore::MIN
215    };
216
217    if let Some(ethics) = ruler_ref.ethics() {
218      if self.building.is_civil() {
219        score *= match ethics.power() {
220          EthicPowerAxis::Militarist => 0.9,
221          EthicPowerAxis::FanaticMilitarist => 0.75,
222          EthicPowerAxis::Pacifist => 1.1,
223          EthicPowerAxis::FanaticPacifist => 1.25,
224        }
225      } else {
226        score *= match ethics.power() {
227          EthicPowerAxis::Militarist => 1.1,
228          EthicPowerAxis::FanaticMilitarist => 1.25,
229          EthicPowerAxis::Pacifist => 0.9,
230          EthicPowerAxis::FanaticPacifist => 0.75,
231        }
232      }
233    }
234
235    Ok(score)
236  }
237
238  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
239    let order = PrefectureBuildOrderRequest {
240      coord: self.coord,
241      building: self.building,
242      kind: PrefectureBuildOrderKind::Construction,
243    };
244
245    world.add_prefecture_build_order(&order)?;
246
247    Ok(ControlFlow::Continue(()))
248  }
249}
250
251#[derive(Clone, Debug, Deserialize, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct BuildStep {
254  id: BuildingId,
255  level: BuildingLevel,
256}
257
258impl BuildStep {
259  fn new(id: BuildingId, level: BuildingLevel) -> Self {
260    Self { id, level }
261  }
262
263  pub fn is_done(&self, infrastructure: &Infrastructure) -> bool {
264    self.level <= infrastructure.building(self.id).level()
265  }
266}
267
268macro_rules! step {
269  ($id:ident, $level: expr) => {{ BuildStep::new(BuildingId::$id, BuildingLevel::new($level)) }};
270}
271
272fn generate_template() -> Vec<BuildStep> {
273  vec![
274    step!(Sawmill, 8),
275    step!(Quarry, 8),
276    step!(IronMine, 8),
277    step!(Prefecture, 2),
278    step!(Sawmill, 10),
279    step!(Quarry, 10),
280    step!(IronMine, 10),
281    step!(Prefecture, 3),
282    step!(Academy, 1),
283    step!(Farm, 2),
284    step!(Wall, 1),
285    step!(Warehouse, 2),
286    step!(Silo, 2),
287    step!(Sawmill, 12),
288    step!(Quarry, 12),
289    step!(IronMine, 12),
290    step!(Wall, 3),
291    step!(Academy, 3),
292    step!(Silo, 4),
293    step!(Farm, 4),
294    step!(Prefecture, 5),
295    step!(Sawmill, 15),
296    step!(Quarry, 15),
297    step!(IronMine, 15),
298    step!(Academy, 5),
299    step!(Warehouse, 11),
300    step!(Wall, 7),
301    step!(Prefecture, 10),
302    step!(Stable, 1),
303    step!(Sawmill, 18),
304    step!(Quarry, 18),
305    step!(IronMine, 18),
306    step!(Stable, 2),
307    step!(Wall, 10),
308    step!(Silo, 6),
309    step!(Farm, 6),
310    step!(Stable, 3),
311    step!(Warehouse, 15),
312    step!(Wall, 15),
313    step!(Sawmill, 20),
314    step!(Quarry, 20),
315    step!(IronMine, 20),
316    step!(Silo, 10),
317    step!(Farm, 10),
318    step!(Warehouse, 18),
319    step!(Wall, 20),
320    step!(Prefecture, 17),
321    step!(Stable, 6),
322    step!(Academy, 8),
323    step!(Sawmill, 25),
324    step!(Quarry, 25),
325    step!(IronMine, 25),
326    step!(Warehouse, 20),
327    step!(Silo, 15),
328    step!(Farm, 15),
329    step!(Stable, 13),
330    step!(Academy, 13),
331    step!(Warehouse, 23),
332    step!(Prefecture, 25),
333    step!(Academy, 20),
334    step!(Silo, 20),
335    step!(Farm, 20),
336    step!(Warehouse, 27),
337    step!(Sawmill, 30),
338    step!(Quarry, 30),
339    step!(IronMine, 30),
340    step!(Silo, 25),
341    step!(Farm, 25),
342    step!(Stable, 20),
343    step!(Academy, 25),
344    step!(Prefecture, 30),
345    step!(Warehouse, 30),
346    step!(Farm, 30),
347    step!(Silo, 30),
348  ]
349}