cuqueclicker_lib/game/tree/
aggregate.rs1use std::collections::HashSet;
10
11use crate::game::fingerer::FINGERERS;
12use crate::game::powerup::N_KINDS;
13use crate::game::tree::coord::TreeCoord;
14use crate::game::tree::node::{NodeSpec, node_at};
15use crate::game::tree::primitive::{Op, Primitive, Target};
16
17#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct FingererTreeContrib {
22 pub flat_fps: f64,
23 pub add_percent: f64,
24 pub mul_factor: f64,
25 pub cost_mul: f64,
28}
29
30impl Default for FingererTreeContrib {
31 fn default() -> Self {
32 Self {
33 flat_fps: 0.0,
34 add_percent: 0.0,
35 mul_factor: 1.0,
36 cost_mul: 1.0,
37 }
38 }
39}
40
41#[derive(Clone, Debug)]
44pub struct TreeAggregate {
45 pub per_fingerer: Vec<FingererTreeContrib>,
47 pub all_fingerers_flat: f64,
50 pub all_fingerers_add: f64,
51 pub all_fingerers_mul: f64,
52 pub click_add: f64,
54 pub click_mul: f64,
55 pub click_flat: f64,
56 pub prestige_add: f64,
59 pub prestige_mul: f64,
60 pub powerup_spawn_mul: [f64; N_KINDS],
62 pub powerup_reward_mul: [f64; N_KINDS],
63 pub powerup_duration_mul: [f64; N_KINDS],
64 pub green_coin_strength_mul: f64,
67}
68
69impl Default for TreeAggregate {
70 fn default() -> Self {
71 Self {
72 per_fingerer: vec![FingererTreeContrib::default(); FINGERERS.len()],
73 all_fingerers_flat: 0.0,
74 all_fingerers_add: 0.0,
75 all_fingerers_mul: 1.0,
76 click_add: 0.0,
77 click_mul: 1.0,
78 click_flat: 0.0,
79 prestige_add: 0.0,
80 prestige_mul: 1.0,
81 powerup_spawn_mul: [1.0; N_KINDS],
82 powerup_reward_mul: [1.0; N_KINDS],
83 powerup_duration_mul: [1.0; N_KINDS],
84 green_coin_strength_mul: 1.0,
85 }
86 }
87}
88
89impl TreeAggregate {
90 pub fn reset(&mut self) {
94 if self.per_fingerer.len() != FINGERERS.len() {
95 self.per_fingerer = vec![FingererTreeContrib::default(); FINGERERS.len()];
96 } else {
97 for c in self.per_fingerer.iter_mut() {
98 *c = FingererTreeContrib::default();
99 }
100 }
101 self.all_fingerers_flat = 0.0;
102 self.all_fingerers_add = 0.0;
103 self.all_fingerers_mul = 1.0;
104 self.click_add = 0.0;
105 self.click_mul = 1.0;
106 self.click_flat = 0.0;
107 self.prestige_add = 0.0;
108 self.prestige_mul = 1.0;
109 self.powerup_spawn_mul = [1.0; N_KINDS];
110 self.powerup_reward_mul = [1.0; N_KINDS];
111 self.powerup_duration_mul = [1.0; N_KINDS];
112 self.green_coin_strength_mul = 1.0;
113 }
114
115 pub fn rebuild_from_bought(&mut self, bought: &HashSet<TreeCoord>) {
120 self.reset();
121 for &lot in bought {
122 if let Some(node) = node_at(lot.x, lot.y) {
123 for &p in &node.primitives {
124 fold_primitive(self, p, true);
125 }
126 }
127 }
128 }
129
130 pub fn fold_in_node(&mut self, node: &NodeSpec) {
134 for &p in &node.primitives {
135 fold_primitive(self, p, true);
136 }
137 }
138
139 pub fn fold_out_node(&mut self, node: &NodeSpec) {
142 for &p in &node.primitives {
143 fold_primitive(self, p, false);
144 }
145 }
146
147 pub fn effective_for_fingerer(&self, idx: usize) -> FingererTreeContrib {
150 let base = self.per_fingerer.get(idx).copied().unwrap_or_default();
151 FingererTreeContrib {
152 flat_fps: base.flat_fps + self.all_fingerers_flat,
153 add_percent: base.add_percent + self.all_fingerers_add,
154 mul_factor: base.mul_factor * self.all_fingerers_mul,
155 cost_mul: base.cost_mul,
156 }
157 }
158}
159
160fn fold_primitive(agg: &mut TreeAggregate, p: Primitive, add: bool) {
161 let sign = if add { 1.0 } else { -1.0 };
162 match (p.op, p.target) {
163 (Op::AddPercent, Target::Fingerer(i)) => {
165 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
166 c.add_percent += sign * p.magnitude;
167 }
168 }
169 (Op::MulFactor, Target::Fingerer(i)) => {
170 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
171 if add {
172 c.mul_factor *= p.magnitude;
173 } else if p.magnitude != 0.0 {
174 c.mul_factor /= p.magnitude;
175 }
176 }
177 }
178 (Op::FlatAdd, Target::Fingerer(i)) => {
179 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
180 c.flat_fps += sign * p.magnitude;
181 }
182 }
183 (Op::CostMul, Target::Fingerer(i)) => {
184 if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
185 if add {
186 c.cost_mul *= p.magnitude;
187 } else if p.magnitude != 0.0 {
188 c.cost_mul /= p.magnitude;
189 }
190 }
191 }
192 (Op::AddPercent, Target::AllFingerers) => agg.all_fingerers_add += sign * p.magnitude,
194 (Op::MulFactor, Target::AllFingerers) => {
195 if add {
196 agg.all_fingerers_mul *= p.magnitude;
197 } else if p.magnitude != 0.0 {
198 agg.all_fingerers_mul /= p.magnitude;
199 }
200 }
201 (Op::FlatAdd, Target::AllFingerers) => agg.all_fingerers_flat += sign * p.magnitude,
202 (Op::AddPercent, Target::Click) => agg.click_add += sign * p.magnitude,
204 (Op::MulFactor, Target::Click) => {
205 if add {
206 agg.click_mul *= p.magnitude;
207 } else if p.magnitude != 0.0 {
208 agg.click_mul /= p.magnitude;
209 }
210 }
211 (Op::FlatAdd, Target::Click) => agg.click_flat += sign * p.magnitude,
212 (Op::AddPercent, Target::Prestige) => agg.prestige_add += sign * p.magnitude,
214 (Op::MulFactor, Target::Prestige) => {
215 if add {
216 agg.prestige_mul *= p.magnitude;
217 } else if p.magnitude != 0.0 {
218 agg.prestige_mul /= p.magnitude;
219 }
220 }
221 (Op::SpawnRateMul, Target::PowerupSpawn(k)) => {
223 let i = k as usize;
224 if add {
225 agg.powerup_spawn_mul[i] *= p.magnitude;
226 } else if p.magnitude != 0.0 {
227 agg.powerup_spawn_mul[i] /= p.magnitude;
228 }
229 }
230 (Op::EffectMul, Target::PowerupReward(k)) => {
231 let i = k as usize;
232 if add {
233 agg.powerup_reward_mul[i] *= p.magnitude;
234 } else if p.magnitude != 0.0 {
235 agg.powerup_reward_mul[i] /= p.magnitude;
236 }
237 }
238 (Op::EffectMul, Target::PowerupDuration(k)) => {
239 let i = k as usize;
240 if add {
241 agg.powerup_duration_mul[i] *= p.magnitude;
242 } else if p.magnitude != 0.0 {
243 agg.powerup_duration_mul[i] /= p.magnitude;
244 }
245 }
246 (Op::EffectMul, Target::GreenCoinStrength) => {
248 if add {
249 agg.green_coin_strength_mul *= p.magnitude;
250 } else if p.magnitude != 0.0 {
251 agg.green_coin_strength_mul /= p.magnitude;
252 }
253 }
254 (op, target) => {
260 debug_assert!(
261 false,
262 "unhandled tree primitive: op={op:?} target={target:?} — \
263 add a fold arm in aggregate.rs::fold_primitive or remove \
264 this (op, target) pairing from procgen pick_op"
265 );
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn p(op: Op, target: Target, mag: f64) -> Primitive {
275 Primitive {
276 op,
277 target,
278 magnitude: mag,
279 }
280 }
281
282 #[test]
283 fn default_is_identity() {
284 let a = TreeAggregate::default();
285 for c in &a.per_fingerer {
286 assert_eq!(c.flat_fps, 0.0);
287 assert_eq!(c.add_percent, 0.0);
288 assert_eq!(c.mul_factor, 1.0);
289 assert_eq!(c.cost_mul, 1.0);
290 }
291 assert_eq!(a.all_fingerers_mul, 1.0);
292 assert_eq!(a.click_mul, 1.0);
293 assert_eq!(a.prestige_mul, 1.0);
294 for v in a.powerup_spawn_mul {
295 assert_eq!(v, 1.0);
296 }
297 }
298
299 #[test]
300 fn fold_in_then_out_returns_to_default() {
301 let mut a = TreeAggregate::default();
302 let prims = vec![
303 p(Op::AddPercent, Target::Fingerer(0), 0.10),
304 p(Op::MulFactor, Target::Click, 2.0),
305 p(Op::EffectMul, Target::GreenCoinStrength, 1.5),
306 ];
307 for &pp in &prims {
308 fold_primitive(&mut a, pp, true);
309 }
310 for &pp in &prims {
311 fold_primitive(&mut a, pp, false);
312 }
313 assert!((a.per_fingerer[0].add_percent).abs() < 1e-12);
316 assert!((a.click_mul - 1.0).abs() < 1e-12);
317 assert!((a.green_coin_strength_mul - 1.0).abs() < 1e-12);
318 }
319
320 #[test]
321 fn add_percent_stacks_additively() {
322 let mut a = TreeAggregate::default();
323 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
324 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.15), true);
325 assert!((a.per_fingerer[0].add_percent - 0.25).abs() < 1e-12);
326 }
327
328 #[test]
329 fn mul_factor_stacks_multiplicatively() {
330 let mut a = TreeAggregate::default();
331 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 2.0), true);
332 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 3.0), true);
333 assert!((a.click_mul - 6.0).abs() < 1e-12);
334 }
335
336 #[test]
337 fn effective_for_fingerer_folds_global() {
338 let mut a = TreeAggregate::default();
339 fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
340 fold_primitive(&mut a, p(Op::AddPercent, Target::AllFingerers, 0.05), true);
341 let eff = a.effective_for_fingerer(0);
342 assert!((eff.add_percent - 0.15).abs() < 1e-12);
343 }
344
345 #[test]
346 fn rebuild_from_empty_bought_is_identity() {
347 let mut a = TreeAggregate::default();
348 fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 5.0), true);
350 a.rebuild_from_bought(&HashSet::new());
351 assert_eq!(a.click_mul, 1.0);
352 }
353
354 #[test]
355 fn rebuild_with_only_anchor_is_identity() {
356 let mut bought = HashSet::new();
362 bought.insert(TreeCoord::ORIGIN);
363 let mut a = TreeAggregate::default();
364 a.rebuild_from_bought(&bought);
365 for c in &a.per_fingerer {
366 assert_eq!(c.add_percent, 0.0);
367 assert_eq!(c.flat_fps, 0.0);
368 assert!((c.mul_factor - 1.0).abs() < 1e-12);
369 assert!((c.cost_mul - 1.0).abs() < 1e-12);
370 }
371 assert!((a.click_mul - 1.0).abs() < 1e-12);
372 assert!((a.all_fingerers_mul - 1.0).abs() < 1e-12);
373 assert!((a.prestige_mul - 1.0).abs() < 1e-12);
374 }
375}