1use std::collections::HashSet;
10
11use crate::bignum::Mag;
12use crate::game::fingerer::FINGERERS;
13use crate::game::powerup::N_KINDS;
14use crate::game::tree::coord::TreeCoord;
15use crate::game::tree::node::{NodeSpec, node_at};
16use crate::game::tree::primitive::{Op, Primitive, Target};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct FingererTreeContrib {
29 pub flat_fps: f64,
30 pub add_percent: f64,
31 pub mul_factor: Mag,
32 pub cost_mul: Mag,
35}
36
37impl Default for FingererTreeContrib {
38 fn default() -> Self {
39 Self {
40 flat_fps: 0.0,
41 add_percent: 0.0,
42 mul_factor: Mag::ONE,
43 cost_mul: Mag::ONE,
44 }
45 }
46}
47
48#[derive(Clone, Debug)]
58pub struct TreeAggregate {
59 pub per_fingerer: Vec<FingererTreeContrib>,
61 pub all_fingerers_flat: f64,
64 pub all_fingerers_add: f64,
65 pub all_fingerers_mul: Mag,
66 pub click_add: f64,
68 pub click_mul: Mag,
69 pub click_flat: f64,
70 pub prestige_add: f64,
73 pub prestige_mul: Mag,
74 pub powerup_spawn_mul: [f64; N_KINDS],
79 pub powerup_reward_mul: [Mag; N_KINDS],
84 pub powerup_duration_mul: [Mag; N_KINDS],
85 pub green_coin_strength_mul: Mag,
88}
89
90impl Default for TreeAggregate {
91 fn default() -> Self {
92 Self {
93 per_fingerer: vec![FingererTreeContrib::default(); FINGERERS.len()],
94 all_fingerers_flat: 0.0,
95 all_fingerers_add: 0.0,
96 all_fingerers_mul: Mag::ONE,
97 click_add: 0.0,
98 click_mul: Mag::ONE,
99 click_flat: 0.0,
100 prestige_add: 0.0,
101 prestige_mul: Mag::ONE,
102 powerup_spawn_mul: [1.0; N_KINDS],
103 powerup_reward_mul: [Mag::ONE; N_KINDS],
104 powerup_duration_mul: [Mag::ONE; N_KINDS],
105 green_coin_strength_mul: Mag::ONE,
106 }
107 }
108}
109
110impl TreeAggregate {
111 pub fn reset(&mut self) {
115 if self.per_fingerer.len() != FINGERERS.len() {
116 self.per_fingerer = vec![FingererTreeContrib::default(); FINGERERS.len()];
117 } else {
118 for c in self.per_fingerer.iter_mut() {
119 *c = FingererTreeContrib::default();
120 }
121 }
122 self.all_fingerers_flat = 0.0;
123 self.all_fingerers_add = 0.0;
124 self.all_fingerers_mul = Mag::ONE;
125 self.click_add = 0.0;
126 self.click_mul = Mag::ONE;
127 self.click_flat = 0.0;
128 self.prestige_add = 0.0;
129 self.prestige_mul = Mag::ONE;
130 self.powerup_spawn_mul = [1.0; N_KINDS];
131 self.powerup_reward_mul = [Mag::ONE; N_KINDS];
132 self.powerup_duration_mul = [Mag::ONE; N_KINDS];
133 self.green_coin_strength_mul = Mag::ONE;
134 }
135
136 pub fn rebuild_from_bought(&mut self, bought: &HashSet<TreeCoord>) {
141 self.reset();
142 for &lot in bought {
143 if let Some(node) = node_at(lot.x, lot.y) {
144 for &p in &node.primitives {
145 fold_primitive(self, p, true);
146 }
147 }
148 }
149 }
150
151 pub fn fold_in_node(&mut self, node: &NodeSpec) {
155 for &p in &node.primitives {
156 fold_primitive(self, p, true);
157 }
158 }
159
160 pub fn fold_out_node(&mut self, node: &NodeSpec) {
163 for &p in &node.primitives {
164 fold_primitive(self, p, false);
165 }
166 }
167
168 pub fn effective_for_fingerer(&self, idx: usize) -> FingererTreeContrib {
171 let base = self.per_fingerer.get(idx).copied().unwrap_or_default();
172 FingererTreeContrib {
173 flat_fps: base.flat_fps + self.all_fingerers_flat,
174 add_percent: base.add_percent + self.all_fingerers_add,
175 mul_factor: base.mul_factor.mul(self.all_fingerers_mul),
176 cost_mul: base.cost_mul,
177 }
178 }
179}
180
181fn fold_primitive(agg: &mut TreeAggregate, p: Primitive, add: bool) {
186 let sign = if add { 1.0 } else { -1.0 };
187 let mag = Mag::from_f64(p.magnitude);
188 let apply_mul = |slot: &mut Mag, m: Mag| {
189 if add {
190 *slot = slot.mul(m);
191 } else if !m.is_zero() {
192 *slot = slot.div(m);
193 }
194 };
195 match (p.op, p.target) {
196 (Op::AddPercent, Target::Fingerer(i)) => {
198 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
199 c.add_percent += sign * p.magnitude;
200 }
201 }
202 (Op::MulFactor, Target::Fingerer(i)) => {
203 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
204 apply_mul(&mut c.mul_factor, mag);
205 }
206 }
207 (Op::FlatAdd, Target::Fingerer(i)) => {
208 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
209 c.flat_fps += sign * p.magnitude;
210 }
211 }
212 (Op::CostMul, Target::Fingerer(i)) => {
213 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
214 apply_mul(&mut c.cost_mul, mag);
215 }
216 }
217 (Op::AddPercent, Target::AllFingerers) => agg.all_fingerers_add += sign * p.magnitude,
219 (Op::MulFactor, Target::AllFingerers) => apply_mul(&mut agg.all_fingerers_mul, mag),
220 (Op::FlatAdd, Target::AllFingerers) => agg.all_fingerers_flat += sign * p.magnitude,
221 (Op::AddPercent, Target::Click) => agg.click_add += sign * p.magnitude,
223 (Op::MulFactor, Target::Click) => apply_mul(&mut agg.click_mul, mag),
224 (Op::FlatAdd, Target::Click) => agg.click_flat += sign * p.magnitude,
225 (Op::AddPercent, Target::Prestige) => agg.prestige_add += sign * p.magnitude,
227 (Op::MulFactor, Target::Prestige) => apply_mul(&mut agg.prestige_mul, mag),
228 (Op::SpawnRateMul, Target::PowerupSpawn(k)) => {
230 let i = k as usize;
231 if add {
232 agg.powerup_spawn_mul[i] *= p.magnitude;
233 } else if p.magnitude != 0.0 {
234 agg.powerup_spawn_mul[i] /= p.magnitude;
235 }
236 }
237 (Op::EffectMul, Target::PowerupReward(k)) => {
238 apply_mul(&mut agg.powerup_reward_mul[k as usize], mag);
239 }
240 (Op::EffectMul, Target::PowerupDuration(k)) => {
241 apply_mul(&mut agg.powerup_duration_mul[k as usize], mag);
242 }
243 (Op::EffectMul, Target::GreenCoinStrength) => {
245 apply_mul(&mut agg.green_coin_strength_mul, mag);
246 }
247 (op, target) => {
253 debug_assert!(
254 false,
255 "unhandled tree primitive: op={op:?} target={target:?} — \
256 add a fold arm in aggregate.rs::fold_primitive or remove \
257 this (op, target) pairing from procgen pick_op"
258 );
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 fn p(op: Op, target: Target, mag: f64) -> Primitive {
268 Primitive {
269 op,
270 target,
271 magnitude: mag,
272 }
273 }
274
275 #[test]
276 fn default_is_identity() {
277 let a = TreeAggregate::default();
278 for c in &a.per_fingerer {
279 assert_eq!(c.flat_fps, 0.0);
280 assert_eq!(c.add_percent, 0.0);
281 assert_eq!(c.mul_factor, Mag::ONE);
282 assert_eq!(c.cost_mul, Mag::ONE);
283 }
284 assert_eq!(a.all_fingerers_mul, Mag::ONE);
285 assert_eq!(a.click_mul, Mag::ONE);
286 assert_eq!(a.prestige_mul, Mag::ONE);
287 for v in a.powerup_spawn_mul {
288 assert_eq!(v, 1.0);
289 }
290 for v in a.powerup_reward_mul {
291 assert_eq!(v, Mag::ONE);
292 }
293 }
294
295 #[test]
296 fn fold_in_then_out_returns_to_default() {
297 let mut a = TreeAggregate::default();
298 let prims = vec![
299 p(Op::AddPercent, Target::Fingerer(0), 0.10),
300 p(Op::MulFactor, Target::Click, 2.0),
301 p(Op::EffectMul, Target::GreenCoinStrength, 1.5),
302 ];
303 for &pp in &prims {
304 fold_primitive(&mut a, pp, true);
305 }
306 for &pp in &prims {
307 fold_primitive(&mut a, pp, false);
308 }
309 assert!((a.per_fingerer[0].add_percent).abs() < 1e-12);
310 assert!((a.click_mul.to_f64() - 1.0).abs() < 1e-12);
311 assert!((a.green_coin_strength_mul.to_f64() - 1.0).abs() < 1e-12);
312 }
313
314 #[test]
315 fn add_percent_stacks_additively() {
316 let mut a = TreeAggregate::default();
317 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
318 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.15), true);
319 assert!((a.per_fingerer[0].add_percent - 0.25).abs() < 1e-12);
320 }
321
322 #[test]
323 fn mul_factor_stacks_multiplicatively() {
324 let mut a = TreeAggregate::default();
325 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 2.0), true);
326 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 3.0), true);
327 assert!((a.click_mul.to_f64() - 6.0).abs() < 1e-9);
328 }
329
330 #[test]
331 fn mul_factor_stack_is_truly_unbounded() {
332 let mut a = TreeAggregate::default();
336 for _ in 0..100_000 {
337 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 1.005), true);
338 }
339 let expected_log10 = (1.005_f64).log10() * 100_000.0;
340 assert!((a.click_mul.log10 - expected_log10).abs() < 1e-6);
341 }
342
343 #[test]
344 fn effective_for_fingerer_folds_global() {
345 let mut a = TreeAggregate::default();
346 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
347 fold_primitive(&mut a, p(Op::AddPercent, Target::AllFingerers, 0.05), true);
348 let eff = a.effective_for_fingerer(0);
349 assert!((eff.add_percent - 0.15).abs() < 1e-12);
350 }
351
352 #[test]
353 fn rebuild_from_empty_bought_is_identity() {
354 let mut a = TreeAggregate::default();
355 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 5.0), true);
357 a.rebuild_from_bought(&HashSet::new());
358 assert_eq!(a.click_mul, Mag::ONE);
359 }
360
361 #[test]
362 fn rebuild_with_only_anchor_is_identity() {
363 let mut bought = HashSet::new();
364 bought.insert(TreeCoord::ORIGIN);
365 let mut a = TreeAggregate::default();
366 a.rebuild_from_bought(&bought);
367 for c in &a.per_fingerer {
368 assert_eq!(c.add_percent, 0.0);
369 assert_eq!(c.flat_fps, 0.0);
370 assert_eq!(c.mul_factor, Mag::ONE);
371 assert_eq!(c.cost_mul, Mag::ONE);
372 }
373 assert_eq!(a.click_mul, Mag::ONE);
374 assert_eq!(a.all_fingerers_mul, Mag::ONE);
375 assert_eq!(a.prestige_mul, Mag::ONE);
376 }
377}