swarm / office/src/sim.rs

raw file 2536 bytes
//! Barrier stepping: the scene freezes while LLM calls are in flight and
//! only advances once every due person's decision has landed.

use bevy::prelude::*;
use std::collections::{HashMap, HashSet};

use crate::llm::DecisionResult;

/// Simulated clock in minutes since day 1, 00:00. Day runs 09:00–17:00.
#[derive(Resource)]
pub struct SimClock(pub f32);

impl SimClock {
    pub fn day(&self) -> u32 {
        (self.0 / 1440.0) as u32 + 1
    }

    pub fn minute_of_day(&self) -> f32 {
        self.0 % 1440.0
    }

    pub fn hhmm(&self) -> String {
        let m = self.minute_of_day() as u32;
        format!("{:02}:{:02}", m / 60, m % 60)
    }
}

pub const DAY_START: f32 = 9.0 * 60.0;
pub const DAY_END: f32 = 17.0 * 60.0;
pub const LUNCH_START: f32 = 12.0 * 60.0;
pub const LUNCH_END: f32 = 13.0 * 60.0;
/// Sim minutes per barrier round.
pub const STEP_MINUTES: f32 = 30.0;
/// Real seconds the scene animates between compute rounds.
pub const STEP_ANIM: f32 = 2.0;
/// Deadman: force-complete a round this late (endpoint died mid-round).
pub const COMPUTE_TIMEOUT: f32 = 240.0;

#[derive(Resource, PartialEq, Clone, Copy, Debug)]
pub enum Phase {
    /// Scene animates: bodies move, bubbles show.
    Stepping,
    /// LLM results pending — world frozen until the round completes.
    Computing,
}

impl Default for Phase {
    fn default() -> Self {
        Phase::Stepping
    }
}

#[derive(Resource, Default)]
pub struct RoundState {
    /// People whose decision has not come back this round.
    pub pending: HashSet<String>,
    /// Work-log expansions still outstanding (split mode only).
    pub pending_writes: usize,
    /// Work decisions waiting on their coder-model content.
    pub deferred: HashMap<String, DecisionResult>,
    pub results: Vec<DecisionResult>,
    pub rounds: u64,
    /// Real-time when computing started.
    pub computing_since: f32,
    /// Real-time until the animation window ends.
    pub step_until: f32,
}

impl RoundState {
    pub fn done(&self) -> bool {
        self.pending.is_empty() && self.pending_writes == 0
    }

    pub fn start_round(&mut self) {
        self.pending.clear();
        self.deferred.clear();
        self.pending_writes = 0;
        self.results.clear();
    }
}

/// Active company-wide event (meeting / standup).
#[derive(Resource, Default)]
pub struct CompanyEvents {
    pub meeting: Option<Meeting>,
    pub meeting_snapped: bool,
}

#[derive(Clone)]
pub struct Meeting {
    pub topic: String,
    pub until: f32, // sim clock minutes
}