use std::sync::Arc;
pub struct Consideration<S> {
pub name: String,
evaluator: Arc<dyn Fn(&S) -> f64 + Send + Sync>,
pub curve: ResponseCurve,
pub weight: f64,
}
impl<S> Clone for Consideration<S> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
evaluator: Arc::clone(&self.evaluator),
curve: self.curve.clone(),
weight: self.weight,
}
}
}
impl<S> std::fmt::Debug for Consideration<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Consideration")
.field("name", &self.name)
.field("weight", &self.weight)
.field("curve", &self.curve)
.finish()
}
}
impl<S> Consideration<S> {
pub fn evaluate(&self, state: &S) -> f64 {
let raw = (self.evaluator)(state);
self.curve.evaluate(raw)
}
pub fn evaluate_traced(&self, state: &S) -> (f64, TraceEntry) {
let raw = (self.evaluator)(state);
let curved = self.curve.evaluate(raw);
let entry = TraceEntry {
name: self.name.clone(),
raw_value: raw,
curved_value: curved,
weight: self.weight,
contribution: 0.0, };
(curved, entry)
}
}
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub name: String,
pub raw_value: f64,
pub curved_value: f64,
pub weight: f64,
pub contribution: f64,
}
#[derive(Debug, Clone)]
pub struct ScoringTrace {
pub action_name: String,
pub base_score: f64,
pub mode: ScoringMode,
pub entries: Vec<TraceEntry>,
pub total_score: f64,
}
impl std::fmt::Display for ScoringTrace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== {} (score: {:.3}, base: {:.3}, mode: {:?}) ===",
self.action_name, self.total_score, self.base_score, self.mode)?;
for e in &self.entries {
writeln!(f, " {:<20} raw={:>8.3} curved={:>8.3} w={:.2} contrib={:>8.3}",
e.name, e.raw_value, e.curved_value, e.weight, e.contribution)?;
}
Ok(())
}
}
#[derive(Clone)]
pub enum ResponseCurve {
Linear { min: f64, max: f64 },
Inverse { steepness: f64 },
Threshold { threshold: f64 },
Boolean,
Constant(f64),
Identity,
Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
}
impl std::fmt::Debug for ResponseCurve {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Linear { min, max } => write!(f, "Linear({min}..{max})"),
Self::Inverse { steepness } => write!(f, "Inverse({steepness})"),
Self::Threshold { threshold } => write!(f, "Threshold({threshold})"),
Self::Boolean => write!(f, "Boolean"),
Self::Constant(v) => write!(f, "Constant({v})"),
Self::Identity => write!(f, "Identity"),
Self::Custom(_) => write!(f, "Custom(fn)"),
}
}
}
impl ResponseCurve {
pub fn evaluate(&self, value: f64) -> f64 {
match self {
ResponseCurve::Linear { min, max } => {
if max == min { return if value >= *min { 1.0 } else { 0.0 }; }
((value - min) / (max - min)).clamp(0.0, 1.0)
}
ResponseCurve::Inverse { steepness } => {
1.0 / (1.0 + value * steepness)
}
ResponseCurve::Threshold { threshold } => {
if value >= *threshold { 1.0 } else { 0.0 }
}
ResponseCurve::Boolean => {
if value > 0.0 { 1.0 } else { 0.0 }
}
ResponseCurve::Constant(v) => *v,
ResponseCurve::Identity => value,
ResponseCurve::Custom(f) => f(value),
}
}
}
pub struct UtilityAction<S> {
pub name: String,
pub considerations: Vec<Consideration<S>>,
pub base_score: f64,
pub mode: ScoringMode,
}
#[derive(Debug, Clone, Copy)]
pub enum ScoringMode {
Multiplicative,
Additive,
}
impl<S> Clone for UtilityAction<S> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
considerations: self.considerations.clone(),
base_score: self.base_score,
mode: self.mode,
}
}
}
impl<S> std::fmt::Debug for UtilityAction<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UtilityAction")
.field("name", &self.name)
.field("base_score", &self.base_score)
.field("mode", &self.mode)
.field("considerations", &self.considerations)
.finish()
}
}
impl<S> UtilityAction<S> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
considerations: Vec::new(),
base_score: 1.0,
mode: ScoringMode::Multiplicative,
}
}
pub fn with_base(mut self, base: f64) -> Self {
self.base_score = base;
self
}
pub fn with_mode(mut self, mode: ScoringMode) -> Self {
self.mode = mode;
self
}
pub fn consider(
mut self,
name: impl Into<String>,
evaluator: impl Fn(&S) -> f64 + Send + Sync + 'static,
curve: ResponseCurve,
weight: f64,
) -> Self {
self.considerations.push(Consideration {
name: name.into(),
evaluator: Arc::new(evaluator),
curve,
weight,
});
self
}
pub fn score(&self, state: &S) -> f64 {
match self.mode {
ScoringMode::Multiplicative => {
let mut total = self.base_score;
for c in &self.considerations {
let curved = c.evaluate(state);
total *= curved * c.weight + (1.0 - c.weight);
}
total
}
ScoringMode::Additive => {
let mut total = self.base_score;
for c in &self.considerations {
let curved = c.evaluate(state);
total += curved * c.weight;
}
total
}
}
}
pub fn score_with_trace(&self, state: &S) -> ScoringTrace {
let mut entries = Vec::with_capacity(self.considerations.len());
let total = match self.mode {
ScoringMode::Multiplicative => {
let mut total = self.base_score;
for c in &self.considerations {
let (curved, mut entry) = c.evaluate_traced(state);
let blended = curved * c.weight + (1.0 - c.weight);
let before = total;
total *= blended;
entry.contribution = total - before;
entries.push(entry);
}
total
}
ScoringMode::Additive => {
let mut total = self.base_score;
for c in &self.considerations {
let (curved, mut entry) = c.evaluate_traced(state);
let contrib = curved * c.weight;
entry.contribution = contrib;
total += contrib;
entries.push(entry);
}
total
}
};
ScoringTrace {
action_name: self.name.clone(),
base_score: self.base_score,
mode: self.mode,
entries,
total_score: total,
}
}
}
pub fn rank_actions<S>(actions: &[UtilityAction<S>], state: &S) -> Vec<(f64, usize)> {
let mut scored: Vec<(f64, usize)> = actions.iter()
.enumerate()
.map(|(i, a)| (a.score(state), i))
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
scored
}
pub fn best_action<S>(actions: &[UtilityAction<S>], state: &S) -> Option<(f64, usize)> {
rank_actions(actions, state).into_iter().next()
}
pub trait ActionSource<A, V> {
fn available_actions(&self, context: &V) -> Vec<A>;
}
pub fn collect_actions<A, V>(sources: &[&dyn ActionSource<A, V>], context: &V) -> Vec<A> {
sources.iter()
.flat_map(|s| s.available_actions(context))
.collect()
}
pub trait AssignmentStrategy {
fn assign(&mut self, scores: &mut Vec<Vec<f64>>) -> Vec<(usize, usize, f64)>;
}
type NoopAdjust = fn(usize, usize, &mut Vec<Vec<f64>>);
fn noop_adjust(_: usize, _: usize, _: &mut Vec<Vec<f64>>) {}
pub struct Greedy<F = NoopAdjust>
where
F: FnMut(usize, usize, &mut Vec<Vec<f64>>),
{
adjust: F,
}
impl Greedy<NoopAdjust> {
pub fn new() -> Self {
Self { adjust: noop_adjust }
}
}
impl Default for Greedy<NoopAdjust> {
fn default() -> Self {
Self::new()
}
}
impl<F: FnMut(usize, usize, &mut Vec<Vec<f64>>)> Greedy<F> {
pub fn with_coordination(adjust: F) -> Self {
Self { adjust }
}
}
impl<F: FnMut(usize, usize, &mut Vec<Vec<f64>>)> AssignmentStrategy for Greedy<F> {
fn assign(&mut self, scores: &mut Vec<Vec<f64>>) -> Vec<(usize, usize, f64)> {
let num_entities = scores.len();
let mut assigned = vec![false; num_entities];
let mut assignments = Vec::new();
for _ in 0..num_entities {
let mut best_score = f64::NEG_INFINITY;
let mut best_e = 0;
let mut best_t = 0;
for (ei, row) in scores.iter().enumerate() {
if assigned[ei] { continue; }
for (ti, &score) in row.iter().enumerate() {
if score > best_score {
best_score = score;
best_e = ei;
best_t = ti;
}
}
}
if best_score == f64::NEG_INFINITY { break; }
assigned[best_e] = true;
assignments.push((best_e, best_t, best_score));
(self.adjust)(best_e, best_t, scores);
}
assignments
}
}
pub struct RoundRobin<F = NoopAdjust>
where
F: FnMut(usize, usize, &mut Vec<Vec<f64>>),
{
order: Option<Vec<usize>>,
adjust: F,
}
impl RoundRobin<NoopAdjust> {
pub fn new() -> Self {
Self { order: None, adjust: noop_adjust }
}
pub fn with_order(order: Vec<usize>) -> Self {
Self { order: Some(order), adjust: noop_adjust }
}
}
impl Default for RoundRobin<NoopAdjust> {
fn default() -> Self {
Self::new()
}
}
impl<F: FnMut(usize, usize, &mut Vec<Vec<f64>>)> RoundRobin<F> {
pub fn with_coordination(order: Option<Vec<usize>>, adjust: F) -> Self {
Self { order, adjust }
}
}
impl<F: FnMut(usize, usize, &mut Vec<Vec<f64>>)> AssignmentStrategy for RoundRobin<F> {
fn assign(&mut self, scores: &mut Vec<Vec<f64>>) -> Vec<(usize, usize, f64)> {
let num_entities = scores.len();
let order: Vec<usize> = self
.order
.clone()
.unwrap_or_else(|| (0..num_entities).collect());
let mut assignments = Vec::new();
for ei in order {
if ei >= num_entities { continue; }
let mut best_score = f64::NEG_INFINITY;
let mut best_t = 0;
for (ti, &score) in scores[ei].iter().enumerate() {
if score > best_score {
best_score = score;
best_t = ti;
}
}
if best_score == f64::NEG_INFINITY { continue; }
assignments.push((ei, best_t, best_score));
(self.adjust)(ei, best_t, scores);
}
assignments
}
}
pub struct Hungarian;
impl Hungarian {
pub fn new() -> Self {
Self
}
}
impl Default for Hungarian {
fn default() -> Self {
Self::new()
}
}
impl AssignmentStrategy for Hungarian {
fn assign(&mut self, scores: &mut Vec<Vec<f64>>) -> Vec<(usize, usize, f64)> {
let n = scores.len();
if n == 0 { return vec![]; }
let m = scores[0].len();
if m == 0 { return vec![]; }
let size = n.max(m);
let mut max_abs = 1.0f64;
for row in scores.iter() {
for &s in row.iter() {
if s != f64::NEG_INFINITY && s.abs() > max_abs {
max_abs = s.abs();
}
}
}
let forbidden = max_abs * (size as f64 + 1.0) + 1.0;
let mut c = vec![vec![forbidden; size]; size];
for i in 0..n {
for j in 0..m {
let s = scores[i][j];
c[i][j] = if s == f64::NEG_INFINITY { forbidden } else { -s };
}
}
let mut u = vec![0.0f64; size + 1];
let mut v = vec![0.0f64; size + 1];
let mut p = vec![0usize; size + 1];
let mut way = vec![0usize; size + 1];
for i in 1..=size {
p[0] = i;
let mut j0 = 0usize;
let mut minv = vec![f64::INFINITY; size + 1];
let mut used = vec![false; size + 1];
loop {
used[j0] = true;
let i0 = p[j0];
let mut delta = f64::INFINITY;
let mut j1 = 0usize;
for j in 1..=size {
if !used[j] {
let cur = c[i0 - 1][j - 1] - u[i0] - v[j];
if cur < minv[j] {
minv[j] = cur;
way[j] = j0;
}
if minv[j] < delta {
delta = minv[j];
j1 = j;
}
}
}
for j in 0..=size {
if used[j] {
u[p[j]] += delta;
v[j] -= delta;
} else {
minv[j] -= delta;
}
}
j0 = j1;
if p[j0] == 0 { break; }
}
loop {
let j1 = way[j0];
p[j0] = p[j1];
j0 = j1;
if j0 == 0 { break; }
}
}
let mut assignments = Vec::new();
for j in 1..=size {
let i = p[j];
if i == 0 { continue; }
let ei = i - 1;
let ti = j - 1;
if ei >= n || ti >= m { continue; }
let s = scores[ei][ti];
if s == f64::NEG_INFINITY { continue; }
assignments.push((ei, ti, s));
}
assignments.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
assignments
}
}
pub struct WeightedRandom {
rng_state: u64,
temperature: f64,
}
impl WeightedRandom {
pub fn new(seed: u64) -> Self {
let rng_state = if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed };
Self { rng_state, temperature: 1.0 }
}
pub fn with_temperature(seed: u64, temperature: f64) -> Self {
let mut s = Self::new(seed);
s.temperature = temperature.max(1.0e-6);
s
}
fn next_u64(&mut self) -> u64 {
let mut x = self.rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.rng_state = x;
x
}
fn next_unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
}
}
impl AssignmentStrategy for WeightedRandom {
fn assign(&mut self, scores: &mut Vec<Vec<f64>>) -> Vec<(usize, usize, f64)> {
let n = scores.len();
if n == 0 { return vec![]; }
let m = scores[0].len();
if m == 0 { return vec![]; }
let mut task_used = vec![false; m];
let mut assignments = Vec::new();
for ei in 0..n {
let mut max_s = f64::NEG_INFINITY;
for (ti, &s) in scores[ei].iter().enumerate() {
if task_used[ti] { continue; }
if s == f64::NEG_INFINITY { continue; }
if s > max_s { max_s = s; }
}
if max_s == f64::NEG_INFINITY { continue; }
let mut total = 0.0f64;
let mut weights: Vec<(usize, f64)> = Vec::new();
for (ti, &s) in scores[ei].iter().enumerate() {
if task_used[ti] { continue; }
if s == f64::NEG_INFINITY { continue; }
let w = ((s - max_s) / self.temperature).exp();
total += w;
weights.push((ti, w));
}
if weights.is_empty() || total <= 0.0 { continue; }
let pick = self.next_unit() * total;
let mut cumulative = 0.0f64;
let mut chosen = weights[0].0;
for (ti, w) in &weights {
cumulative += *w;
if cumulative >= pick {
chosen = *ti;
break;
}
}
task_used[chosen] = true;
assignments.push((ei, chosen, scores[ei][chosen]));
}
assignments
}
}