nil_core/behavior/impl/
trade.rs1use 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 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}