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.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 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}