use crate::accounting::{self, Ledger};
use crate::work_cycle::{self, Action, Stage, State, Submission};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimConfig {
pub cycles: usize,
pub cost_per_cycle: u128,
pub revenue_per_accepted_task: u128,
pub submit_quality: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CycleSnapshot {
pub cycle: usize,
pub treasury: u128,
pub tasks_accepted_this_cycle: usize,
pub actions_count: usize,
pub net_position: i128,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Forecast {
pub snapshots: Vec<CycleSnapshot>,
pub total_accepted: usize,
pub ran_out_at: Option<usize>,
pub final_treasury: u128,
pub final_ledger: Ledger,
}
pub fn simulate(initial: State, cfg: &SimConfig) -> Forecast {
let seed_capital = initial.treasury; let mut state = initial;
let mut snapshots: Vec<CycleSnapshot> = Vec::new();
let mut total_accepted: usize = 0;
let mut ran_out_at: Option<usize> = None;
let mut cum_costs: u128 = 0;
let mut cum_revenue: u128 = 0;
for cycle in 0..cfg.cycles {
if state.treasury < cfg.cost_per_cycle {
ran_out_at = Some(cycle);
break;
}
let assigned: Vec<u64> = state
.backlog
.tasks
.iter()
.filter_map(|t| matches!(t.stage, Stage::Assigned { .. }).then_some(t.id))
.collect();
for id in assigned {
state.deliver(id, Submission { quality: cfg.submit_quality, claims_impossible: false });
}
let (next, actions) = work_cycle::step(&state);
state = next;
let accepts =
actions.iter().filter(|a| matches!(a, Action::AcceptResult { .. })).count();
let payouts: u128 = actions
.iter()
.map(|a| if let Action::Payout { amount, .. } = a { *amount } else { 0 })
.fold(0u128, u128::saturating_add);
let revenue = cfg.revenue_per_accepted_task.saturating_mul(accepts as u128);
state.treasury = state.treasury.saturating_add(revenue);
state.treasury = state.treasury.saturating_sub(cfg.cost_per_cycle);
cum_costs = cum_costs.saturating_add(cfg.cost_per_cycle).saturating_add(payouts);
cum_revenue = cum_revenue.saturating_add(revenue);
total_accepted += accepts;
let ledger = Ledger {
treasury: state.treasury,
period_costs: cum_costs,
period_revenue: cum_revenue,
seed_capital,
};
snapshots.push(CycleSnapshot {
cycle,
treasury: state.treasury,
tasks_accepted_this_cycle: accepts,
actions_count: actions.len(),
net_position: accounting::net_position(&ledger),
});
}
Forecast {
snapshots,
total_accepted,
ran_out_at,
final_treasury: state.treasury,
final_ledger: Ledger {
treasury: state.treasury,
period_costs: cum_costs,
period_revenue: cum_revenue,
seed_capital,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::work_cycle::{Backlog, Criteria, Role, Task, WorkerState};
fn task(id: u64, reward: u128, min_quality: u8, stage: Stage) -> Task {
Task {
id,
role: Role::Coder,
reward,
min_reputation: 0,
criteria: Criteria { min_quality },
stage,
}
}
fn worker(id: u64, available: bool) -> WorkerState {
WorkerState { id, role: Role::Coder, reputation: 0, available }
}
fn state(tasks: Vec<Task>, workers: Vec<WorkerState>, treasury: u128) -> State {
State { backlog: Backlog { tasks }, workers, treasury }
}
#[test]
fn high_cost_empty_backlog_runs_out_and_flags_the_cycle() {
let cfg = SimConfig {
cycles: 10,
cost_per_cycle: 30,
revenue_per_accepted_task: 0,
submit_quality: 5,
};
let f = simulate(state(vec![], vec![], 100), &cfg);
assert_eq!(f.ran_out_at, Some(3));
assert_eq!(f.snapshots.len(), 3); assert_eq!(f.final_treasury, 10);
assert_eq!(f.total_accepted, 0);
assert!(f.snapshots.iter().all(|s| s.actions_count == 0));
assert_eq!(f.snapshots.iter().map(|s| s.treasury).collect::<Vec<_>>(), vec![70, 40, 10]);
assert_eq!(f.final_ledger.period_costs, 90);
assert_eq!(f.final_ledger.period_revenue, 0);
assert_eq!(f.final_ledger.seed_capital, 100);
assert!(accounting::relies_on_seed(&f.final_ledger));
assert!(!accounting::is_self_funding(&f.final_ledger));
}
#[test]
fn exact_fit_funds_every_cycle_no_runout() {
let cfg = SimConfig {
cycles: 5,
cost_per_cycle: 20,
revenue_per_accepted_task: 0,
submit_quality: 5,
};
let f = simulate(state(vec![], vec![], 100), &cfg);
assert_eq!(f.ran_out_at, None);
assert_eq!(f.snapshots.len(), 5);
assert_eq!(f.final_treasury, 0);
}
#[test]
fn revenue_above_cost_keeps_treasury_growing() {
let cfg = SimConfig {
cycles: 6,
cost_per_cycle: 5,
revenue_per_accepted_task: 100,
submit_quality: 5,
};
let initial = state(
vec![task(1, 10, 3, Stage::Assigned { worker_id: 7 })],
vec![worker(7, false)],
1_000,
);
let f = simulate(initial, &cfg);
assert_eq!(f.ran_out_at, None);
assert_eq!(f.total_accepted, 1);
assert_eq!(f.final_treasury, 1_060);
assert!(f.final_treasury > 1_000); assert!(accounting::is_self_funding(&f.final_ledger));
assert!(!accounting::relies_on_seed(&f.final_ledger));
assert!(accounting::net_position(&f.final_ledger) > 0);
}
#[test]
fn zero_cost_with_revenue_is_monotonic_nondecreasing() {
let cfg = SimConfig {
cycles: 4,
cost_per_cycle: 0,
revenue_per_accepted_task: 100,
submit_quality: 5,
};
let initial = state(
vec![task(1, 10, 3, Stage::Assigned { worker_id: 7 })],
vec![worker(7, false)],
500,
);
let f = simulate(initial, &cfg);
assert_eq!(f.ran_out_at, None);
let mut prev = 500u128;
for s in &f.snapshots {
assert!(s.treasury >= prev, "treasury dipped at cycle {}", s.cycle);
prev = s.treasury;
}
assert_eq!(f.final_treasury, 590); }
#[test]
fn empty_backlog_zero_cost_is_fully_quiescent() {
let cfg = SimConfig {
cycles: 5,
cost_per_cycle: 0,
revenue_per_accepted_task: 100,
submit_quality: 5,
};
let f = simulate(state(vec![], vec![worker(7, true)], 250), &cfg);
assert_eq!(f.ran_out_at, None);
assert_eq!(f.snapshots.len(), 5);
assert_eq!(f.total_accepted, 0);
assert!(f.snapshots.iter().all(|s| s.actions_count == 0));
assert!(f.snapshots.iter().all(|s| s.tasks_accepted_this_cycle == 0));
assert!(f.snapshots.iter().all(|s| s.treasury == 250)); assert_eq!(f.final_treasury, 250);
assert_eq!(f.final_ledger.period_revenue, 0);
assert_eq!(f.final_ledger.period_costs, 0);
}
#[test]
fn zero_cycles_yields_an_empty_forecast() {
let cfg = SimConfig {
cycles: 0,
cost_per_cycle: 5,
revenue_per_accepted_task: 100,
submit_quality: 5,
};
let f = simulate(state(vec![], vec![], 42), &cfg);
assert!(f.snapshots.is_empty());
assert_eq!(f.ran_out_at, None);
assert_eq!(f.total_accepted, 0);
assert_eq!(f.final_treasury, 42);
assert_eq!(f.final_ledger.seed_capital, 42);
}
#[test]
fn submit_quality_at_or_above_bar_accepts_and_earns() {
let cfg = SimConfig {
cycles: 3,
cost_per_cycle: 0,
revenue_per_accepted_task: 100,
submit_quality: 5,
};
let initial = state(
vec![task(1, 10, 4, Stage::Assigned { worker_id: 7 })],
vec![worker(7, false)],
1_000,
);
let f = simulate(initial, &cfg);
assert_eq!(f.total_accepted, 1);
assert_eq!(f.snapshots[0].tasks_accepted_this_cycle, 1);
assert_eq!(f.final_treasury, 1_090); assert_eq!(f.final_ledger.period_revenue, 100);
assert_eq!(f.final_ledger.period_costs, 10); }
#[test]
fn submit_quality_below_bar_rejects_and_earns_nothing() {
let cfg = SimConfig {
cycles: 3,
cost_per_cycle: 0,
revenue_per_accepted_task: 100,
submit_quality: 2,
};
let initial = state(
vec![task(1, 10, 4, Stage::Assigned { worker_id: 7 })],
vec![worker(7, false)],
1_000,
);
let f = simulate(initial, &cfg);
assert_eq!(f.total_accepted, 0);
assert_eq!(f.snapshots[0].tasks_accepted_this_cycle, 0);
assert_eq!(f.final_treasury, 1_000); assert_eq!(f.final_ledger.period_revenue, 0);
assert_eq!(f.final_ledger.period_costs, 0);
}
#[test]
fn planned_task_takes_post_assign_deliver_cycles_to_accept() {
let cfg = SimConfig {
cycles: 5,
cost_per_cycle: 1,
revenue_per_accepted_task: 50,
submit_quality: 5,
};
let initial =
state(vec![task(1, 10, 3, Stage::Planned)], vec![worker(7, true)], 1_000);
let f = simulate(initial, &cfg);
assert_eq!(f.ran_out_at, None);
assert_eq!(f.total_accepted, 1);
assert_eq!(f.snapshots[2].tasks_accepted_this_cycle, 1);
assert_eq!(f.snapshots[0].tasks_accepted_this_cycle, 0);
assert_eq!(f.snapshots[1].tasks_accepted_this_cycle, 0);
assert_eq!(f.final_treasury, 1_035);
}
#[test]
fn extreme_values_saturate_without_panicking() {
let cfg = SimConfig {
cycles: 3,
cost_per_cycle: u128::MAX,
revenue_per_accepted_task: u128::MAX,
submit_quality: 5,
};
let initial = state(
vec![task(1, u128::MAX, 1, Stage::Assigned { worker_id: 7 })],
vec![worker(7, false)],
u128::MAX,
);
let f = simulate(initial, &cfg);
assert_eq!(f.ran_out_at, Some(1));
assert_eq!(f.snapshots.len(), 1);
assert_eq!(f.final_ledger.period_costs, u128::MAX);
}
}