use bevy::app::{App, FixedUpdate, Plugin};
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::message::MessageReader;
use bevy::ecs::query::QueryData;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::system::{Query, Res, ResMut};
use bevy::log::error;
use bevy::reflect::Reflect;
use crate::core::{
PersonIndex, SocietyClock, SocietySettings, SocietySystems, Tuning, require_core,
};
use crate::defs::{SocietyDefs, TopicId};
use crate::model::{opinion, rumor_dk};
use crate::relationships::{Tie, Ties};
use crate::reputation::NormViolated;
use crate::rumor::{PlantRumor, RumorBook, RumorId, RumorLearned, plant_rumor};
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Opinion {
pub value: f32,
pub anchor: f32,
pub stubbornness: f32,
}
impl Default for Opinion {
fn default() -> Self {
Self {
value: 0.0,
anchor: 0.0,
stubbornness: 0.2,
}
}
}
impl Opinion {
pub fn at(anchor: f32) -> Self {
Self {
value: anchor,
anchor,
..Self::default()
}
}
pub fn influence(&mut self, other: f32, openness: f32, weight: f32, tuning: &Tuning) -> f32 {
let d = other - self.value;
let epsilon =
opinion::confidence_bound(tuning.confidence_base, tuning.confidence_openness, openness);
self.value +=
opinion::deffuant_step(self.value, other, epsilon, tuning.influence_mu, weight);
self.value += opinion::anchor_step(
tuning.anchor_pull,
self.stubbornness,
self.anchor,
self.value,
);
self.value = self.value.clamp(-1.0, 1.0);
opinion::similarity_affinity_step(tuning.similarity_gain, d)
}
pub fn conform(
&mut self,
group_mean: f32,
n_in_contact: usize,
agreeableness: f32,
dt_days: f32,
tuning: &Tuning,
) {
if !opinion::conformity_applies(group_mean) {
return;
}
self.value += opinion::conformity_step(
tuning.conformity_per_day,
agreeableness,
n_in_contact,
dt_days,
group_mean,
self.value,
);
self.value = self.value.clamp(-1.0, 1.0);
}
}
#[derive(Component, Debug, Clone, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Opinions(pub Vec<Opinion>);
impl Opinions {
pub fn get(&self, topic: usize) -> Option<&Opinion> {
self.0.get(topic)
}
pub fn get_mut(&mut self, topic: usize) -> Option<&mut Opinion> {
self.0.get_mut(topic)
}
pub fn value(&self, topic: usize) -> f32 {
self.0.get(topic).map_or(0.0, |o| o.value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Known {
pub rumor: RumorId,
pub belief: f32,
pub spreader: bool,
pub heard_day: f32,
}
#[derive(Component, Debug, Clone, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Knowledge(pub Vec<Known>);
impl Knowledge {
fn search(&self, rumor: RumorId) -> Result<usize, usize> {
self.0.binary_search_by_key(&rumor.0, |k| k.rumor.0)
}
pub fn get(&self, rumor: RumorId) -> Option<&Known> {
self.search(rumor).ok().map(|i| &self.0[i])
}
pub fn knows(&self, rumor: RumorId) -> bool {
self.search(rumor).is_ok()
}
pub fn insert(&mut self, known: Known, capacity: usize, now_days: f32) {
if capacity == 0 {
self.0.clear();
return;
}
match self.search(known.rumor) {
Ok(i) => {
self.0[i] = known;
return;
}
Err(i) => self.0.insert(i, known),
}
while self.0.len() > capacity {
let worst = self
.0
.iter()
.enumerate()
.map(|(i, k)| (i, Self::retention(k, now_days)))
.fold(None::<(usize, f32)>, |acc, (i, w)| match acc {
Some((_, best)) if best <= w => acc,
_ => Some((i, w)),
});
match worst {
Some((i, _)) => {
self.0.remove(i);
}
None => break,
}
}
}
fn retention(known: &Known, now_days: f32) -> f32 {
let hours = (now_days - known.heard_day).max(0.0) * 24.0;
known.belief * 0.995_f32.powf(hours)
}
pub fn get_mut(&mut self, rumor: RumorId) -> Option<&mut Known> {
match self.search(rumor) {
Ok(i) => Some(&mut self.0[i]),
Err(_) => None,
}
}
pub fn best_telling(
&self,
now_days: f32,
eligible: impl Fn(RumorId) -> Option<f32>,
) -> Option<RumorId> {
let mut best: Option<(f32, RumorId)> = None;
for known in self.0.iter().filter(|k| k.spreader) {
let Some(valence) = eligible(known.rumor) else {
continue;
};
let hours = (now_days - known.heard_day).max(0.0) * 24.0;
let weight = rumor_dk::telling_weight(valence, known.belief, hours);
match best {
Some((w, _)) if weight <= w => {}
_ => best = Some((weight, known.rumor)),
}
}
best.map(|(_, r)| r)
}
pub fn forget(&mut self, rumor: RumorId) {
if let Ok(i) = self.search(rumor) {
self.0.remove(i);
}
}
}
pub fn belief_after_hearing(teller_belief: f32, trust: f32) -> f32 {
rumor_dk::belief_after_hearing(teller_belief, trust)
}
pub(crate) fn influence_weight(ties: &Ties, other: Entity, tuning: &crate::Tuning) -> f32 {
let tie = ties.get(other).copied().unwrap_or_else(|| Tie::new(other));
0.5 * tie.trust + 0.5 * tuning.affinity_weight(tie.affinity).max(0.0)
}
pub(crate) fn exchange_opinions(
opinions: &mut Opinions,
ties: &mut Ties,
openness: f32,
other: Entity,
other_values: &[(TopicId, f32)],
tuning: &Tuning,
) {
let weight = influence_weight(ties, other, tuning);
let mut delta = 0.0;
for &(topic, other_value) in other_values {
let t = usize::from(topic.0);
if let Some(opinion) = opinions.get_mut(t) {
delta += opinion.influence(other_value, openness, weight, tuning);
}
}
let tie = ties.entry(other);
tie.add_affinity(delta, tuning);
}
pub(crate) fn snapshot_values(opinions: &Opinions, topics: &[TopicId]) -> Vec<(TopicId, f32)> {
topics
.iter()
.map(|&topic| (topic, opinions.value(usize::from(topic.0))))
.collect()
}
pub struct BeliefsPlugin;
impl Plugin for BeliefsPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Opinion>()
.register_type::<Opinions>()
.register_type::<Known>()
.register_type::<Knowledge>()
.register_type::<RumorId>()
.register_type::<crate::rumor::Rumor>()
.register_type::<RumorBook>()
.init_resource::<RumorBook>()
.add_message::<PlantRumor>()
.add_message::<RumorLearned>()
.add_message::<NormViolated>()
.add_systems(
FixedUpdate,
apply_planted_rumors.in_set(SocietySystems::Clock),
)
.add_systems(
FixedUpdate,
conform_opinions.in_set(SocietySystems::Propagate),
);
}
fn finish(&self, app: &mut App) {
require_core(app, "BeliefsPlugin");
}
}
fn apply_planted_rumors(
mut reader: MessageReader<PlantRumor>,
defs: Res<SocietyDefs>,
settings: Res<SocietySettings>,
clock: Res<SocietyClock>,
mut book: ResMut<RumorBook>,
mut knowledge: Query<&mut Knowledge>,
) {
for msg in reader.read() {
let Some(deed) = defs.deed_id(&msg.deed) else {
error!(
"bevy_society: PlantRumor names unknown deed '{}'; ignored",
msg.deed
);
continue;
};
let Ok(mut origin_knowledge) = knowledge.get_mut(msg.origin) else {
error!(
"bevy_society: PlantRumor origin {:?} is not a person; ignored",
msg.origin
);
continue;
};
let Some(spec) = defs.deeds.get(usize::from(deed.0)) else {
continue;
};
plant_rumor(
&mut book,
&mut origin_knowledge,
msg.origin,
msg.subject,
msg.patient,
deed,
spec.harm,
clock.now_days as f32,
msg.truth,
settings.knowledge_capacity,
);
}
}
#[derive(QueryData)]
#[query_data(mutable)]
struct Conformer {
opinions: &'static mut Opinions,
personality: &'static crate::mind::Personality,
}
fn conformity_means(
index: &PersonIndex,
perception: &crate::Perception,
people: &Query<Conformer>,
ties: &Query<&Ties>,
topics: usize,
tuning: &crate::Tuning,
) -> Vec<(Vec<f32>, usize)> {
let mut out = Vec::with_capacity(index.sorted.len());
for (i, &(_, entity)) in index.sorted.iter().enumerate() {
let mut sums = vec![(0.0f32, 0.0f32); topics];
let mut n = 0usize;
let Ok(owned) = ties.get(entity) else {
out.push((vec![0.0; topics], 0));
continue;
};
for &j in perception.contact.get(i).map_or(&[][..], Vec::as_slice) {
if j as usize == i {
continue;
}
let Some(&other) = perception.persons.get(j as usize) else {
continue;
};
let Ok(view) = people.get(other) else {
continue;
};
n += 1;
let affinity = tuning.affinity_weight(owned.get(other).map_or(0.0, |t| t.affinity));
let weight = (affinity + 1.0).max(0.1);
for (t, slot) in sums.iter_mut().enumerate() {
slot.0 += view.opinions.value(t) * weight;
slot.1 += weight;
}
}
let means = sums
.into_iter()
.map(|(sum, weight)| if weight > 0.0 { sum / weight } else { 0.0 })
.collect();
out.push((means, n));
}
out
}
fn conform_opinions(
clock: Res<SocietyClock>,
settings: Res<SocietySettings>,
defs: Res<SocietyDefs>,
index: Res<PersonIndex>,
perception: Res<crate::Perception>,
mut people: Query<Conformer>,
ties: Query<&Ties>,
) {
let topics = defs.topics.len();
if topics == 0 {
return;
}
let means = conformity_means(
&index,
&perception,
&people,
&ties,
topics,
&settings.tuning,
);
let dt = clock.dt_days;
for (i, &(_, entity)) in index.sorted.iter().enumerate() {
let Some(&(ref topic_means, n)) = means.get(i) else {
continue;
};
if n < 3 {
continue;
}
let Ok(mut view) = people.get_mut(entity) else {
continue;
};
let personality = view.personality;
for (t, &mean) in topic_means.iter().enumerate() {
if let Some(opinion) = view.opinions.get_mut(t) {
opinion.conform(mean, n, personality.agreeableness, dt, &settings.tuning);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opinions_converge_when_within_the_bound() {
let tuning = Tuning::default();
let mut a = Opinion::at(0.1);
let mut b = Opinion::at(0.3);
a.stubbornness = 0.0;
b.stubbornness = 0.0;
for _ in 0..50 {
let (av, bv) = (a.value, b.value);
a.influence(bv, 0.5, 1.0, &tuning);
b.influence(av, 0.5, 1.0, &tuning);
}
assert!((a.value - b.value).abs() < 0.01);
}
#[test]
fn distant_opinions_do_not_move() {
let tuning = Tuning::default();
let mut a = Opinion::at(-0.6);
a.stubbornness = 0.0;
a.influence(0.6, 0.5, 1.0, &tuning);
assert!((a.value + 0.6).abs() < 1e-6);
}
#[test]
fn stubborn_anchor_holds() {
let tuning = Tuning::default();
let mut a = Opinion::at(0.1);
a.stubbornness = 1.0;
for _ in 0..200 {
a.influence(0.2, 1.0, 1.0, &tuning);
}
assert!((a.value - a.anchor).abs() < 0.15);
}
#[test]
fn knowledge_stays_sorted_and_tells_the_heaviest_rumor() {
let mut k = Knowledge::default();
for (id, belief) in [(3u32, 0.4f32), (1, 0.9), (2, 0.9)] {
k.insert(
Known {
rumor: RumorId(id),
belief,
spreader: true,
heard_day: 0.0,
},
usize::MAX,
0.0,
);
}
let ids: Vec<u32> = k.0.iter().map(|x| x.rumor.0).collect();
assert_eq!(ids, vec![1, 2, 3]);
let pick = k.best_telling(0.0, |_| Some(-1.0));
assert_eq!(pick, Some(RumorId(1)));
let pick = k.best_telling(0.0, |r| if r.0 == 1 { None } else { Some(-1.0) });
assert_eq!(pick, Some(RumorId(2)));
}
}