use std::collections::HashSet;
use crate::bignum::Mag;
use crate::game::fingerer::FINGERERS;
use crate::game::powerup::N_KINDS;
use crate::game::tree::coord::TreeCoord;
use crate::game::tree::node::{NodeSpec, node_at};
use crate::game::tree::primitive::{Op, Primitive, Target};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FingererTreeContrib {
pub flat_fps: f64,
pub add_percent: f64,
pub mul_factor: Mag,
pub cost_mul: Mag,
}
impl Default for FingererTreeContrib {
fn default() -> Self {
Self {
flat_fps: 0.0,
add_percent: 0.0,
mul_factor: Mag::ONE,
cost_mul: Mag::ONE,
}
}
}
#[derive(Clone, Debug)]
pub struct TreeAggregate {
pub per_fingerer: Vec<FingererTreeContrib>,
pub all_fingerers_flat: f64,
pub all_fingerers_add: f64,
pub all_fingerers_mul: Mag,
pub click_add: f64,
pub click_mul: Mag,
pub click_flat: f64,
pub prestige_add: f64,
pub prestige_mul: Mag,
pub powerup_spawn_mul: [f64; N_KINDS],
pub powerup_reward_mul: [Mag; N_KINDS],
pub powerup_duration_mul: [Mag; N_KINDS],
pub green_coin_strength_mul: Mag,
}
impl Default for TreeAggregate {
fn default() -> Self {
Self {
per_fingerer: vec![FingererTreeContrib::default(); FINGERERS.len()],
all_fingerers_flat: 0.0,
all_fingerers_add: 0.0,
all_fingerers_mul: Mag::ONE,
click_add: 0.0,
click_mul: Mag::ONE,
click_flat: 0.0,
prestige_add: 0.0,
prestige_mul: Mag::ONE,
powerup_spawn_mul: [1.0; N_KINDS],
powerup_reward_mul: [Mag::ONE; N_KINDS],
powerup_duration_mul: [Mag::ONE; N_KINDS],
green_coin_strength_mul: Mag::ONE,
}
}
}
impl TreeAggregate {
pub fn reset(&mut self) {
if self.per_fingerer.len() != FINGERERS.len() {
self.per_fingerer = vec![FingererTreeContrib::default(); FINGERERS.len()];
} else {
for c in self.per_fingerer.iter_mut() {
*c = FingererTreeContrib::default();
}
}
self.all_fingerers_flat = 0.0;
self.all_fingerers_add = 0.0;
self.all_fingerers_mul = Mag::ONE;
self.click_add = 0.0;
self.click_mul = Mag::ONE;
self.click_flat = 0.0;
self.prestige_add = 0.0;
self.prestige_mul = Mag::ONE;
self.powerup_spawn_mul = [1.0; N_KINDS];
self.powerup_reward_mul = [Mag::ONE; N_KINDS];
self.powerup_duration_mul = [Mag::ONE; N_KINDS];
self.green_coin_strength_mul = Mag::ONE;
}
pub fn rebuild_from_bought(&mut self, bought: &HashSet<TreeCoord>) {
self.reset();
for &lot in bought {
if let Some(node) = node_at(lot.x, lot.y) {
for &p in &node.primitives {
fold_primitive(self, p, true);
}
}
}
}
pub fn fold_in_node(&mut self, node: &NodeSpec) {
for &p in &node.primitives {
fold_primitive(self, p, true);
}
}
pub fn fold_out_node(&mut self, node: &NodeSpec) {
for &p in &node.primitives {
fold_primitive(self, p, false);
}
}
pub fn effective_for_fingerer(&self, idx: usize) -> FingererTreeContrib {
let base = self.per_fingerer.get(idx).copied().unwrap_or_default();
FingererTreeContrib {
flat_fps: base.flat_fps + self.all_fingerers_flat,
add_percent: base.add_percent + self.all_fingerers_add,
mul_factor: base.mul_factor.mul(self.all_fingerers_mul),
cost_mul: base.cost_mul,
}
}
}
fn fold_primitive(agg: &mut TreeAggregate, p: Primitive, add: bool) {
let sign = if add { 1.0 } else { -1.0 };
let mag = Mag::from_f64(p.magnitude);
let apply_mul = |slot: &mut Mag, m: Mag| {
if add {
*slot = slot.mul(m);
} else if !m.is_zero() {
*slot = slot.div(m);
}
};
match (p.op, p.target) {
(Op::AddPercent, Target::Fingerer(i)) => {
if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
c.add_percent += sign * p.magnitude;
}
}
(Op::MulFactor, Target::Fingerer(i)) => {
if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
apply_mul(&mut c.mul_factor, mag);
}
}
(Op::FlatAdd, Target::Fingerer(i)) => {
if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
c.flat_fps += sign * p.magnitude;
}
}
(Op::CostMul, Target::Fingerer(i)) => {
if let Some(c) = agg.per_fingerer.get_mut(i as usize) {
apply_mul(&mut c.cost_mul, mag);
}
}
(Op::AddPercent, Target::AllFingerers) => agg.all_fingerers_add += sign * p.magnitude,
(Op::MulFactor, Target::AllFingerers) => apply_mul(&mut agg.all_fingerers_mul, mag),
(Op::FlatAdd, Target::AllFingerers) => agg.all_fingerers_flat += sign * p.magnitude,
(Op::AddPercent, Target::Click) => agg.click_add += sign * p.magnitude,
(Op::MulFactor, Target::Click) => apply_mul(&mut agg.click_mul, mag),
(Op::FlatAdd, Target::Click) => agg.click_flat += sign * p.magnitude,
(Op::AddPercent, Target::Prestige) => agg.prestige_add += sign * p.magnitude,
(Op::MulFactor, Target::Prestige) => apply_mul(&mut agg.prestige_mul, mag),
(Op::SpawnRateMul, Target::PowerupSpawn(k)) => {
let i = k as usize;
if add {
agg.powerup_spawn_mul[i] *= p.magnitude;
} else if p.magnitude != 0.0 {
agg.powerup_spawn_mul[i] /= p.magnitude;
}
}
(Op::EffectMul, Target::PowerupReward(k)) => {
apply_mul(&mut agg.powerup_reward_mul[k as usize], mag);
}
(Op::EffectMul, Target::PowerupDuration(k)) => {
apply_mul(&mut agg.powerup_duration_mul[k as usize], mag);
}
(Op::EffectMul, Target::GreenCoinStrength) => {
apply_mul(&mut agg.green_coin_strength_mul, mag);
}
(op, target) => {
debug_assert!(
false,
"unhandled tree primitive: op={op:?} target={target:?} — \
add a fold arm in aggregate.rs::fold_primitive or remove \
this (op, target) pairing from procgen pick_op"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(op: Op, target: Target, mag: f64) -> Primitive {
Primitive {
op,
target,
magnitude: mag,
}
}
#[test]
fn default_is_identity() {
let a = TreeAggregate::default();
for c in &a.per_fingerer {
assert_eq!(c.flat_fps, 0.0);
assert_eq!(c.add_percent, 0.0);
assert_eq!(c.mul_factor, Mag::ONE);
assert_eq!(c.cost_mul, Mag::ONE);
}
assert_eq!(a.all_fingerers_mul, Mag::ONE);
assert_eq!(a.click_mul, Mag::ONE);
assert_eq!(a.prestige_mul, Mag::ONE);
for v in a.powerup_spawn_mul {
assert_eq!(v, 1.0);
}
for v in a.powerup_reward_mul {
assert_eq!(v, Mag::ONE);
}
}
#[test]
fn fold_in_then_out_returns_to_default() {
let mut a = TreeAggregate::default();
let prims = vec![
p(Op::AddPercent, Target::Fingerer(0), 0.10),
p(Op::MulFactor, Target::Click, 2.0),
p(Op::EffectMul, Target::GreenCoinStrength, 1.5),
];
for &pp in &prims {
fold_primitive(&mut a, pp, true);
}
for &pp in &prims {
fold_primitive(&mut a, pp, false);
}
assert!((a.per_fingerer[0].add_percent).abs() < 1e-12);
assert!((a.click_mul.to_f64() - 1.0).abs() < 1e-12);
assert!((a.green_coin_strength_mul.to_f64() - 1.0).abs() < 1e-12);
}
#[test]
fn add_percent_stacks_additively() {
let mut a = TreeAggregate::default();
fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.15), true);
assert!((a.per_fingerer[0].add_percent - 0.25).abs() < 1e-12);
}
#[test]
fn mul_factor_stacks_multiplicatively() {
let mut a = TreeAggregate::default();
fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 2.0), true);
fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 3.0), true);
assert!((a.click_mul.to_f64() - 6.0).abs() < 1e-9);
}
#[test]
fn mul_factor_stack_is_truly_unbounded() {
let mut a = TreeAggregate::default();
for _ in 0..100_000 {
fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 1.005), true);
}
let expected_log10 = (1.005_f64).log10() * 100_000.0;
assert!((a.click_mul.log10 - expected_log10).abs() < 1e-6);
}
#[test]
fn effective_for_fingerer_folds_global() {
let mut a = TreeAggregate::default();
fold_primitive(&mut a, p(Op::AddPercent, Target::Fingerer(0), 0.10), true);
fold_primitive(&mut a, p(Op::AddPercent, Target::AllFingerers, 0.05), true);
let eff = a.effective_for_fingerer(0);
assert!((eff.add_percent - 0.15).abs() < 1e-12);
}
#[test]
fn rebuild_from_empty_bought_is_identity() {
let mut a = TreeAggregate::default();
fold_primitive(&mut a, p(Op::MulFactor, Target::Click, 5.0), true);
a.rebuild_from_bought(&HashSet::new());
assert_eq!(a.click_mul, Mag::ONE);
}
#[test]
fn rebuild_with_only_anchor_is_identity() {
let mut bought = HashSet::new();
bought.insert(TreeCoord::ORIGIN);
let mut a = TreeAggregate::default();
a.rebuild_from_bought(&bought);
for c in &a.per_fingerer {
assert_eq!(c.add_percent, 0.0);
assert_eq!(c.flat_fps, 0.0);
assert_eq!(c.mul_factor, Mag::ONE);
assert_eq!(c.cost_mul, Mag::ONE);
}
assert_eq!(a.click_mul, Mag::ONE);
assert_eq!(a.all_fingerers_mul, Mag::ONE);
assert_eq!(a.prestige_mul, Mag::ONE);
}
}