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.1;
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 if amount > capacity * Self::SELL_THRESHOLD {
95        match id {
96          ResourceId::Food => push!(SellResourcesBehavior, Food),
97          ResourceId::Iron => push!(SellResourcesBehavior, Iron),
98          ResourceId::Stone => push!(SellResourcesBehavior, Stone),
99          ResourceId::Wood => push!(SellResourcesBehavior, Wood),
100        }
101      }
102    }
103
104    BehaviorProcessor::new(world, behaviors).try_each()?;
105
106    Ok(ControlFlow::Break(()))
107  }
108}
109
110#[derive(Builder, Debug)]
111pub struct BuyResourcesBehavior<T>
112where
113  T: Resource + Debug,
114{
115  ruler: Ruler,
116  resource: ResourceId,
117  marker: PhantomData<T>,
118}
119
120impl<T> BuyResourcesBehavior<T>
121where
122  T: Resource + Debug,
123{
124  fn threshold(&self, world: &World) -> Result<f64> {
125    let capacity = world
126      .get_storage_capacity_for(&self.ruler, self.resource)?
127      .conv::<f64>();
128
129    Ok(capacity.mul_ceil(TradeBehavior::BUY_THRESHOLD))
130  }
131
132  fn shortage(&self, world: &World) -> Result<f64> {
133    let resource = world
134      .ruler(&self.ruler)?
135      .resources()
136      .get(self.resource)
137      .as_f64();
138
139    Ok(self.threshold(world)?.sub(resource).max(0.0))
140  }
141}
142
143impl<T> Behavior for BuyResourcesBehavior<T>
144where
145  T: Resource + Debug + 'static,
146{
147  fn score(&self, world: &World) -> Result<BehaviorScore> {
148    let ruler_ref = world.ruler(&self.ruler)?;
149    let market_price = ruler_ref
150      .resources()
151      .get(self.resource)
152      .market_price();
153
154    if ruler_ref.gold() < market_price {
155      return Ok(BehaviorScore::MIN);
156    }
157
158    let threshold = self.threshold(world)?;
159    let shortage = self.shortage(world)?;
160
161    if threshold <= 0.0 || shortage <= 0.0 {
162      return Ok(BehaviorScore::MIN);
163    }
164
165    Ok(BehaviorScore::new(shortage / threshold))
166  }
167
168  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
169    let ruler_ref = world.ruler(&self.ruler)?;
170    let gold = ruler_ref.gold();
171
172    macro_rules! buyable_amount {
173      ($resource:ident) => {{
174        world
175          .market()
176          .buyable_amount($resource::MARKET_PRICE, gold)
177          .pipe($resource::new)
178          .as_resources()
179      }};
180    }
181
182    let mut buy_amount = match self.resource {
183      ResourceId::Food => buyable_amount!(Food),
184      ResourceId::Iron => buyable_amount!(Iron),
185      ResourceId::Stone => buyable_amount!(Stone),
186      ResourceId::Wood => buyable_amount!(Wood),
187    };
188
189    let shortage = self.shortage(world)?.floor();
190    match self.resource {
191      ResourceId::Food => {
192        Resource::clamp(&mut buy_amount.food, 0, *Food::from(shortage));
193      }
194      ResourceId::Iron => {
195        Resource::clamp(&mut buy_amount.iron, 0, *Iron::from(shortage));
196      }
197      ResourceId::Stone => {
198        Resource::clamp(&mut buy_amount.stone, 0, *Stone::from(shortage));
199      }
200      ResourceId::Wood => {
201        Resource::clamp(&mut buy_amount.wood, 0, *Wood::from(shortage));
202      }
203    }
204
205    let vault_resources = world.market().vault().resources();
206
207    buy_amount.iter_mut().for_each(|resource| {
208      let in_vault = vault_resources.get(resource.id());
209      Resource::clamp(resource, 0, in_vault.as_u32());
210    });
211
212    if !buy_amount.is_empty() {
213      world.buy_resources_with_emit(&self.ruler, buy_amount, false)?;
214    }
215
216    Ok(ControlFlow::Break(()))
217  }
218}
219
220#[derive(Builder, Debug)]
221pub struct SellResourcesBehavior<T>
222where
223  T: Resource + Debug,
224{
225  ruler: Ruler,
226  resource: ResourceId,
227  marker: PhantomData<T>,
228}
229
230impl<T> SellResourcesBehavior<T>
231where
232  T: Resource + Debug,
233{
234  fn threshold(&self, world: &World) -> Result<f64> {
235    let capacity = world
236      .get_storage_capacity_for(&self.ruler, self.resource)?
237      .conv::<f64>();
238
239    Ok(capacity.mul_ceil(TradeBehavior::SELL_THRESHOLD))
240  }
241
242  fn surplus(&self, world: &World, threshold: Option<f64>) -> Result<f64> {
243    let resource = world
244      .ruler(&self.ruler)?
245      .resources()
246      .get(self.resource)
247      .as_f64();
248
249    let threshold = threshold.unwrap_or_try_else(|| self.threshold(world))?;
250
251    Ok(resource.sub(threshold).max(0.0))
252  }
253}
254
255impl<T> Behavior for SellResourcesBehavior<T>
256where
257  T: Resource + Debug + 'static,
258{
259  fn score(&self, world: &World) -> Result<BehaviorScore> {
260    let threshold = self.threshold(world)?;
261    let surplus = self.surplus(world, Some(threshold))?;
262
263    if threshold <= 0.0 || surplus <= 0.0 {
264      return Ok(BehaviorScore::MIN);
265    }
266
267    Ok(BehaviorScore::new(surplus / threshold))
268  }
269
270  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
271    let surplus = self.surplus(world, None)?.floor();
272    if surplus < 1.0 {
273      return Ok(ControlFlow::Break(()));
274    }
275
276    let sell_amount = match self.resource {
277      ResourceId::Food => Food::from(surplus).as_resources(),
278      ResourceId::Iron => Iron::from(surplus).as_resources(),
279      ResourceId::Stone => Stone::from(surplus).as_resources(),
280      ResourceId::Wood => Wood::from(surplus).as_resources(),
281    };
282
283    if !sell_amount.is_empty() {
284      world.sell_resources_with_emit(&self.ruler, sell_amount, false)?;
285    }
286
287    Ok(ControlFlow::Break(()))
288  }
289}