nil-core 0.5.5

Multiplayer strategy game
Documentation
// Copyright (C) Call of Nil contributors
// SPDX-License-Identifier: AGPL-3.0-only

use crate::behavior::r#impl::idle::IdleBehavior;
use crate::behavior::score::BehaviorScore;
use crate::behavior::{Behavior, BehaviorProcessor};
use crate::continent::Coord;
use crate::error::Result;
use crate::ethic::EthicPowerAxis;
use crate::infrastructure::building::StorageId;
use crate::infrastructure::building::r#impl::prefecture::build_queue::{
  PrefectureBuildOrderKind,
  PrefectureBuildOrderRequest,
};
use crate::infrastructure::prelude::*;
use crate::infrastructure::queue::InfrastructureQueue;
use crate::military::maneuver::Maneuver;
use crate::world::World;
use bon::Builder;
use nil_util::iter::IterExt;
use rand::random_range;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::ControlFlow;
use std::sync::LazyLock;
use strum::IntoEnumIterator;

pub(crate) static BUILD_TEMPLATE: LazyLock<Vec<BuildStep>> = LazyLock::new(generate_template);

#[derive(Builder, Debug)]
pub struct BuildBehavior {
  coord: Coord,
}

impl BuildBehavior {
  const MAX_IN_QUEUE: u8 = 5;
}

impl Behavior for BuildBehavior {
  fn score(&self, world: &World) -> Result<BehaviorScore> {
    let config = world.config();
    let infrastructure = world.infrastructure(self.coord)?;
    let max_in_queue = f64::from(Self::MAX_IN_QUEUE);

    if let Some(in_queue) = infrastructure
      .prefecture()
      .turns_in_build_queue(&config)
    {
      Ok(BehaviorScore::new(1.0 - (in_queue / max_in_queue)))
    } else {
      Ok(BehaviorScore::MIN)
    }
  }

  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
    let mut behaviors = vec![IdleBehavior.boxed()];
    macro_rules! push {
      ($building:ident, $id:expr) => {{
        let behavior = BuildBuildingBehavior::builder()
          .marker(PhantomData::<$building>)
          .coord(self.coord)
          .building($id)
          .build()
          .boxed();

        behaviors.push(behavior);
      }};
    }

    for id in BuildingId::iter() {
      match id {
        BuildingId::Academy => push!(Academy, id),
        BuildingId::Farm => push!(Farm, id),
        BuildingId::IronMine => push!(IronMine, id),
        BuildingId::Prefecture => push!(Prefecture, id),
        BuildingId::Quarry => push!(Quarry, id),
        BuildingId::Sawmill => push!(Sawmill, id),
        BuildingId::Silo => push!(Silo, id),
        BuildingId::Stable => push!(Stable, id),
        BuildingId::Wall => push!(Wall, id),
        BuildingId::Warehouse => push!(Warehouse, id),
        BuildingId::Workshop => push!(Workshop, id),
      }
    }

    BehaviorProcessor::new(world, behaviors)
      .take(usize::from(Self::MAX_IN_QUEUE))
      .try_each()?;

    Ok(ControlFlow::Break(()))
  }
}

#[derive(Builder, Debug)]
pub struct BuildBuildingBehavior<T>
where
  T: Building + Debug,
{
  coord: Coord,
  building: BuildingId,
  marker: PhantomData<T>,
}

impl<T> BuildBuildingBehavior<T>
where
  T: Building + Debug,
{
  const STORAGE_CAPACITY_THRESHOLD: f64 = 0.8;
}

impl<T> Behavior for BuildBuildingBehavior<T>
where
  T: Building + Debug + 'static,
{
  #[allow(clippy::too_many_lines)]
  fn score(&self, world: &World) -> Result<BehaviorScore> {
    let infrastructure = world.infrastructure(self.coord)?;
    let building = infrastructure.building(self.building);

    if !building
      .infrastructure_requirements()
      .has_required_levels(infrastructure)
    {
      return Ok(BehaviorScore::MIN);
    }

    let level = infrastructure
      .prefecture()
      .resolve_level(self.building, building.level());

    if level >= building.max_level() {
      return Ok(BehaviorScore::MIN);
    }

    let stats = world.stats().infrastructure();
    let owner = world.continent().owner_of(self.coord)?;
    let ruler_ref = world.ruler(owner)?;

    let required_resources = &stats
      .building(self.building)?
      .get(level + 1u8)?
      .resources;

    if !ruler_ref.has_resources(required_resources) {
      return Ok(BehaviorScore::MIN);
    }

    // Prioritize the wall if there are incoming attacks.
    // It will also check whether the build order can be
    // completed before the nearest one arrives.
    if let BuildingId::Wall = self.building
      && let Some(distance) = world
        .military()
        .maneuvers()
        .filter(|maneuver| maneuver.destination() == self.coord)
        .filter(|maneuver| maneuver.is_attack() && maneuver.is_going())
        .filter_map(Maneuver::pending_distance)
        .min()
    {
      let workforce = stats
        .building(self.building)?
        .get(level + 1u8)
        .map(|it| f64::from(it.workforce))?;

      if workforce <= f64::from(distance) {
        return Ok(BehaviorScore::MAX);
      }
    }

    if let BuildingId::Farm = self.building
      && !world
        .get_maintenance_balance(owner.clone())?
        .is_sustainable()
    {
      return Ok(BehaviorScore::MAX);
    }

    // Prioritize storage when its capacity is almost full.
    if let Ok(id) = StorageId::try_from(self.building) {
      let resources = ruler_ref.resources();
      let capacity = world.get_storage_capacity(owner.clone())?;

      let ratio = match id {
        StorageId::Silo => f64::from(resources.food) / f64::from(capacity.silo),
        StorageId::Warehouse => {
          let capacity = f64::from(capacity.warehouse);
          let iron_ratio = f64::from(resources.iron) / capacity;
          let stone_ratio = f64::from(resources.stone) / capacity;
          let wood_ratio = f64::from(resources.wood) / capacity;
          iron_ratio.max(stone_ratio).max(wood_ratio)
        }
      };

      if ratio >= Self::STORAGE_CAPACITY_THRESHOLD
        && infrastructure
          .prefecture()
          .build_queue()
          .iter()
          .filter(|order| order.kind().is_construction())
          .all(|order| order.building() != self.building)
      {
        return Ok(BehaviorScore::MAX);
      }
    }

    let mut score = if BUILD_TEMPLATE
      .iter()
      .filter(|step| !step.is_done(infrastructure))
      .take(3)
      .any(|step| step.id == self.building)
    {
      BehaviorScore::new(random_range(0.8..=1.0))
    } else {
      BehaviorScore::MIN
    };

    if let Some(ethics) = ruler_ref.ethics() {
      if self.building.is_civil() {
        score *= match ethics.power() {
          EthicPowerAxis::Militarist => 0.9,
          EthicPowerAxis::FanaticMilitarist => 0.75,
          EthicPowerAxis::Pacifist => 1.1,
          EthicPowerAxis::FanaticPacifist => 1.25,
        }
      } else {
        score *= match ethics.power() {
          EthicPowerAxis::Militarist => 1.1,
          EthicPowerAxis::FanaticMilitarist => 1.25,
          EthicPowerAxis::Pacifist => 0.9,
          EthicPowerAxis::FanaticPacifist => 0.75,
        }
      }
    }

    Ok(score)
  }

  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
    let order = PrefectureBuildOrderRequest {
      coord: self.coord,
      building: self.building,
      kind: PrefectureBuildOrderKind::Construction,
    };

    world.add_prefecture_build_order(&order)?;

    Ok(ControlFlow::Continue(()))
  }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct BuildStep {
  id: BuildingId,
  level: BuildingLevel,
}

impl BuildStep {
  const fn new(id: BuildingId, level: BuildingLevel) -> Self {
    Self { id, level }
  }

  pub fn is_done(&self, infrastructure: &Infrastructure) -> bool {
    self.level <= infrastructure.building(self.id).level()
  }
}

macro_rules! step {
  ($id:ident, $level: expr) => {{ BuildStep::new(BuildingId::$id, BuildingLevel::new($level)) }};
}

fn generate_template() -> Vec<BuildStep> {
  vec![
    step!(Sawmill, 8),
    step!(Quarry, 8),
    step!(IronMine, 8),
    step!(Prefecture, 2),
    step!(Sawmill, 10),
    step!(Quarry, 10),
    step!(IronMine, 10),
    step!(Prefecture, 3),
    step!(Academy, 1),
    step!(Farm, 2),
    step!(Wall, 1),
    step!(Warehouse, 2),
    step!(Silo, 2),
    step!(Sawmill, 12),
    step!(Quarry, 12),
    step!(IronMine, 12),
    step!(Wall, 3),
    step!(Academy, 3),
    step!(Silo, 4),
    step!(Farm, 4),
    step!(Prefecture, 5),
    step!(Sawmill, 15),
    step!(Quarry, 15),
    step!(IronMine, 15),
    step!(Academy, 5),
    step!(Warehouse, 11),
    step!(Wall, 7),
    step!(Prefecture, 10),
    step!(Stable, 1),
    step!(Sawmill, 18),
    step!(Quarry, 18),
    step!(IronMine, 18),
    step!(Stable, 2),
    step!(Wall, 10),
    step!(Silo, 6),
    step!(Farm, 6),
    step!(Stable, 3),
    step!(Warehouse, 15),
    step!(Wall, 15),
    step!(Sawmill, 20),
    step!(Quarry, 20),
    step!(IronMine, 20),
    step!(Silo, 10),
    step!(Farm, 10),
    step!(Warehouse, 18),
    step!(Wall, 20),
    step!(Prefecture, 17),
    step!(Stable, 6),
    step!(Academy, 8),
    step!(Sawmill, 25),
    step!(Quarry, 25),
    step!(IronMine, 25),
    step!(Warehouse, 20),
    step!(Silo, 15),
    step!(Farm, 15),
    step!(Stable, 13),
    step!(Academy, 13),
    step!(Warehouse, 23),
    step!(Prefecture, 25),
    step!(Academy, 20),
    step!(Silo, 20),
    step!(Farm, 20),
    step!(Warehouse, 27),
    step!(Sawmill, 30),
    step!(Quarry, 30),
    step!(IronMine, 30),
    step!(Silo, 25),
    step!(Farm, 25),
    step!(Stable, 20),
    step!(Academy, 25),
    step!(Prefecture, 30),
    step!(Warehouse, 30),
    step!(Farm, 30),
    step!(Silo, 30),
  ]
}