Skip to main content

nil_core/behavior/impl/
trade.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::behavior::r#impl::idle::IdleBehavior;
5use crate::behavior::score::BehaviorScore;
6use crate::behavior::{Behavior, BehaviorProcessor};
7use crate::error::Result;
8use crate::resources::prelude::*;
9use crate::ruler::Ruler;
10use crate::world::World;
11use bon::Builder;
12use nil_num::mul_ceil::MulCeil;
13use nil_util::iter::IterExt;
14use nil_util::ops::TryExt;
15use std::fmt::Debug;
16use std::marker::PhantomData;
17use std::ops::{ControlFlow, Sub};
18use strum::IntoEnumIterator;
19use tap::{Conv, Pipe};
20
21#[derive(Builder, Debug)]
22pub struct TradeBehavior {
23  ruler: Ruler,
24}
25
26impl TradeBehavior {
27  pub const BUY_THRESHOLD: f64 = 0.3;
28  pub const SELL_THRESHOLD: f64 = 0.9;
29}
30
31impl Behavior for TradeBehavior {
32  fn score(&self, world: &World) -> Result<BehaviorScore> {
33    if !self.ruler.is_bot() {
34      return Ok(BehaviorScore::MIN);
35    }
36
37    let resources = world
38      .ruler(&self.ruler)?
39      .resources()
40      .sum()
41      .conv::<f64>();
42
43    let capacity = world
44      .get_storage_capacity(&self.ruler)?
45      .mean();
46
47    if capacity <= 0.0 {
48      return Ok(BehaviorScore::MIN);
49    }
50
51    // The score increases as the resources approach the capacity, or when they approach zero.
52    let score = (2.0 * (resources / capacity) - 1.0).powi(2);
53
54    Ok(BehaviorScore::from(score))
55  }
56
57  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
58    let mut behaviors = vec![IdleBehavior.boxed()];
59
60    macro_rules! push {
61      ($behavior:ident, $resource:ident) => {{
62        let behavior = $behavior::builder()
63          .ruler(self.ruler.clone())
64          .resource(ResourceId::$resource)
65          .marker(PhantomData::<$resource>)
66          .build();
67
68        behaviors.push(behavior.boxed());
69      }};
70    }
71
72    let ruler_ref = world.ruler(&self.ruler)?;
73    let resources = ruler_ref.resources();
74    let gold = ruler_ref.gold();
75
76    let vault = world.market().vault().resources();
77
78    for id in ResourceId::iter() {
79      let resource = resources.get(id);
80      let amount = resource.as_f64();
81      let capacity = world
82        .get_storage_capacity_for(&self.ruler, id)?
83        .conv::<f64>();
84
85      let in_vault = vault.get(id).as_u32();
86
87      if in_vault > 0 && gold > 0 && amount < (capacity * Self::BUY_THRESHOLD) {
88        match id {
89          ResourceId::Food => push!(BuyResourcesBehavior, Food),
90          ResourceId::Iron => push!(BuyResourcesBehavior, Iron),
91          ResourceId::Stone => push!(BuyResourcesBehavior, Stone),
92          ResourceId::Wood => push!(BuyResourcesBehavior, Wood),
93        }
94      } else {
95        let threshold = capacity * Self::SELL_THRESHOLD;
96        let surplus = (amount - threshold).floor().max(0.0) as u32;
97        if surplus > 0
98          && in_vault.checked_add(surplus).is_some()
99          && gold.checked_add(surplus).is_some()
100        {
101          match id {
102            ResourceId::Food => push!(SellResourcesBehavior, Food),
103            ResourceId::Iron => push!(SellResourcesBehavior, Iron),
104            ResourceId::Stone => push!(SellResourcesBehavior, Stone),
105            ResourceId::Wood => push!(SellResourcesBehavior, Wood),
106          }
107        }
108      }
109    }
110
111    BehaviorProcessor::new(world, behaviors).try_each()?;
112
113    Ok(ControlFlow::Break(()))
114  }
115}
116
117#[derive(Builder, Debug)]
118pub struct BuyResourcesBehavior<T>
119where
120  T: Resource + Debug,
121{
122  ruler: Ruler,
123  resource: ResourceId,
124  marker: PhantomData<T>,
125}
126
127impl<T> BuyResourcesBehavior<T>
128where
129  T: Resource + Debug,
130{
131  fn threshold(&self, world: &World) -> Result<f64> {
132    let capacity = world
133      .get_storage_capacity_for(&self.ruler, self.resource)?
134      .conv::<f64>();
135
136    Ok(capacity.mul_ceil(TradeBehavior::BUY_THRESHOLD))
137  }
138
139  fn shortage(&self, world: &World) -> Result<f64> {
140    let resource = world
141      .ruler(&self.ruler)?
142      .resources()
143      .get(self.resource)
144      .as_f64();
145
146    Ok(self.threshold(world)?.sub(resource).max(0.0))
147  }
148}
149
150impl<T> Behavior for BuyResourcesBehavior<T>
151where
152  T: Resource + Debug + 'static,
153{
154  fn score(&self, world: &World) -> Result<BehaviorScore> {
155    let ruler_ref = world.ruler(&self.ruler)?;
156    let market_price = ruler_ref
157      .resources()
158      .get(self.resource)
159      .market_price();
160
161    if ruler_ref.gold() < market_price {
162      return Ok(BehaviorScore::MIN);
163    }
164
165    let threshold = self.threshold(world)?;
166    let shortage = self.shortage(world)?;
167
168    if threshold <= 0.0 || shortage <= 0.0 {
169      return Ok(BehaviorScore::MIN);
170    }
171
172    Ok(BehaviorScore::new(shortage / threshold))
173  }
174
175  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
176    let ruler_ref = world.ruler(&self.ruler)?;
177    let gold = ruler_ref.gold();
178
179    macro_rules! buyable_amount {
180      ($resource:ident) => {{
181        world
182          .market()
183          .buyable_amount($resource::MARKET_PRICE, gold)
184          .pipe($resource::new)
185          .as_resources()
186      }};
187    }
188
189    let mut buy_amount = match self.resource {
190      ResourceId::Food => buyable_amount!(Food),
191      ResourceId::Iron => buyable_amount!(Iron),
192      ResourceId::Stone => buyable_amount!(Stone),
193      ResourceId::Wood => buyable_amount!(Wood),
194    };
195
196    let shortage = self.shortage(world)?.floor();
197    match self.resource {
198      ResourceId::Food => {
199        Resource::clamp(&mut buy_amount.food, 0, *Food::from(shortage));
200      }
201      ResourceId::Iron => {
202        Resource::clamp(&mut buy_amount.iron, 0, *Iron::from(shortage));
203      }
204      ResourceId::Stone => {
205        Resource::clamp(&mut buy_amount.stone, 0, *Stone::from(shortage));
206      }
207      ResourceId::Wood => {
208        Resource::clamp(&mut buy_amount.wood, 0, *Wood::from(shortage));
209      }
210    }
211
212    let vault_resources = world.market().vault().resources();
213
214    buy_amount.iter_mut().for_each(|resource| {
215      let in_vault = vault_resources.get(resource.id());
216      Resource::clamp(resource, 0, in_vault.as_u32());
217    });
218
219    if !buy_amount.is_empty() {
220      world.buy_resources_with_emit(&self.ruler, buy_amount, false)?;
221    }
222
223    Ok(ControlFlow::Break(()))
224  }
225}
226
227#[derive(Builder, Debug)]
228pub struct SellResourcesBehavior<T>
229where
230  T: Resource + Debug,
231{
232  ruler: Ruler,
233  resource: ResourceId,
234  marker: PhantomData<T>,
235}
236
237impl<T> SellResourcesBehavior<T>
238where
239  T: Resource + Debug,
240{
241  fn threshold(&self, world: &World) -> Result<f64> {
242    let capacity = world
243      .get_storage_capacity_for(&self.ruler, self.resource)?
244      .conv::<f64>();
245
246    Ok(capacity.mul_ceil(TradeBehavior::SELL_THRESHOLD))
247  }
248
249  fn surplus(&self, world: &World, threshold: Option<f64>) -> Result<f64> {
250    let resource = world
251      .ruler(&self.ruler)?
252      .resources()
253      .get(self.resource)
254      .as_f64();
255
256    let threshold = threshold.unwrap_or_try_else(|| self.threshold(world))?;
257
258    Ok(resource.sub(threshold).max(0.0))
259  }
260}
261
262impl<T> Behavior for SellResourcesBehavior<T>
263where
264  T: Resource + Debug + 'static,
265{
266  fn score(&self, world: &World) -> Result<BehaviorScore> {
267    let threshold = self.threshold(world)?;
268    let surplus = self.surplus(world, Some(threshold))?;
269
270    if threshold <= 0.0 || surplus <= 0.0 {
271      return Ok(BehaviorScore::MIN);
272    }
273
274    Ok(BehaviorScore::new(surplus / threshold))
275  }
276
277  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
278    let surplus = self.surplus(world, None)?.floor();
279    if surplus < 1.0 {
280      return Ok(ControlFlow::Break(()));
281    }
282
283    let sell_amount = match self.resource {
284      ResourceId::Food => Food::from(surplus).as_resources(),
285      ResourceId::Iron => Iron::from(surplus).as_resources(),
286      ResourceId::Stone => Stone::from(surplus).as_resources(),
287      ResourceId::Wood => Wood::from(surplus).as_resources(),
288    };
289
290    if !sell_amount.is_empty() {
291      world.sell_resources_with_emit(&self.ruler, sell_amount, false)?;
292    }
293
294    Ok(ControlFlow::Break(()))
295  }
296}