Skip to main content

canwu_sim/
random.rs

1use crate::{CanwuError, ErrorCode};
2use canwu_core::{ArmyId, BoundaryId, DeterministicRng, EventId, PersonId, RandomDrawId};
3use canwu_event::CauseRef;
4use canwu_time::SimTime;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8const STREAM_DERIVATION_DOMAIN: &[u8] = b"canwu.random-stream.v1";
9
10#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum RandomAlgorithm {
13    #[default]
14    SplitMix64V1,
15}
16
17#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
18pub struct RandomStreamKey {
19    pub namespace: String,
20    pub name: String,
21    pub version: u32,
22}
23
24impl RandomStreamKey {
25    #[must_use]
26    pub fn new(namespace: impl Into<String>, name: impl Into<String>, version: u32) -> Self {
27        Self {
28            namespace: namespace.into(),
29            name: name.into(),
30            version,
31        }
32    }
33}
34
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub struct RandomStreamState {
37    pub key: RandomStreamKey,
38    pub algorithm: RandomAlgorithm,
39    pub seed: u64,
40    pub position: u64,
41    pub generator_state: u64,
42}
43
44impl RandomStreamState {
45    pub(crate) fn initial(root_seed: u64, key: RandomStreamKey) -> Self {
46        let seed = derive_stream_seed(root_seed, &key);
47        Self {
48            key,
49            algorithm: RandomAlgorithm::SplitMix64V1,
50            seed,
51            position: 0,
52            generator_state: seed,
53        }
54    }
55
56    pub(crate) fn is_coherent(&self, root_seed: u64) -> bool {
57        self.algorithm == RandomAlgorithm::SplitMix64V1
58            && self.seed == derive_stream_seed(root_seed, &self.key)
59            && self.generator_state == DeterministicRng::state_after(self.seed, self.position)
60    }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum RandomDrawProducer {
66    BoundarySystem {
67        boundary: BoundaryId,
68        plugin: String,
69        system: String,
70    },
71    CoreSystem {
72        system: String,
73    },
74}
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum RandomDrawOutcome {
79    BoundarySystemDecision,
80    KnowledgeReportDelivery {
81        recipient: PersonId,
82        army: ArmyId,
83        dispatch_event: EventId,
84        arrives_at: SimTime,
85    },
86}
87
88#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
89pub struct RandomDrawRecord {
90    pub id: RandomDrawId,
91    pub at: SimTime,
92    pub stream: RandomStreamKey,
93    pub position: u64,
94    pub upper_exclusive: u64,
95    pub value: u64,
96    pub purpose: String,
97    pub producer: RandomDrawProducer,
98    #[serde(default)]
99    pub outcome: Option<RandomDrawOutcome>,
100    pub cause: CauseRef,
101    pub correlation_id: u64,
102}
103
104#[derive(Clone, Debug)]
105pub(crate) struct PendingRandomDraw {
106    pub stream: RandomStreamKey,
107    pub position: u64,
108    pub upper_exclusive: u64,
109    pub value: u64,
110    pub purpose: String,
111}
112
113pub(crate) struct RandomExecution {
114    pub states: BTreeMap<RandomStreamKey, RandomStreamState>,
115    pub draws: Vec<PendingRandomDraw>,
116}
117
118pub(crate) struct RandomSession {
119    states: BTreeMap<RandomStreamKey, RandomStreamState>,
120    draws: Vec<PendingRandomDraw>,
121}
122
123impl RandomSession {
124    pub(crate) fn new(
125        available: &BTreeMap<RandomStreamKey, RandomStreamState>,
126        allowed: &[RandomStreamKey],
127    ) -> Result<Self, CanwuError> {
128        let mut states = BTreeMap::new();
129        for key in allowed {
130            let Some(state) = available.get(key) else {
131                return Err(CanwuError::new(
132                    ErrorCode::InvalidRandomStream,
133                    format!(
134                        "declared random stream {}.{}@{} is not initialized",
135                        key.namespace, key.name, key.version
136                    ),
137                ));
138            };
139            states.insert(key.clone(), state.clone());
140        }
141        Ok(Self {
142            states,
143            draws: Vec::new(),
144        })
145    }
146
147    pub(crate) fn range(
148        &mut self,
149        key: &RandomStreamKey,
150        upper_exclusive: u64,
151        purpose: &str,
152    ) -> Result<u64, CanwuError> {
153        if upper_exclusive == 0 || purpose.trim().is_empty() || purpose != purpose.trim() {
154            return Err(CanwuError::new(
155                ErrorCode::InvalidRandomDraw,
156                "random draws require a positive bound and canonical purpose",
157            ));
158        }
159        let Some(state) = self.states.get_mut(key) else {
160            return Err(CanwuError::new(
161                ErrorCode::UndeclaredRandomStream,
162                format!(
163                    "random stream {}.{}@{} was not declared by this system",
164                    key.namespace, key.name, key.version
165                ),
166            ));
167        };
168        let next_position = state.position.checked_add(1).ok_or_else(|| {
169            CanwuError::new(
170                ErrorCode::IdentifierExhausted,
171                "random stream position is exhausted",
172            )
173        })?;
174        let position = state.position;
175        let mut generator = DeterministicRng::from_seed(state.generator_state);
176        let value = generator.range(upper_exclusive);
177        state.position = next_position;
178        state.generator_state = generator.state();
179        self.draws.push(PendingRandomDraw {
180            stream: key.clone(),
181            position,
182            upper_exclusive,
183            value,
184            purpose: purpose.to_owned(),
185        });
186        Ok(value)
187    }
188
189    pub(crate) fn finish(self) -> RandomExecution {
190        RandomExecution {
191            states: self.states,
192            draws: self.draws,
193        }
194    }
195}
196
197pub(crate) fn derive_stream_seed(root_seed: u64, key: &RandomStreamKey) -> u64 {
198    if key.namespace == "canwu.core" && key.name == "knowledge-report-delay" && key.version == 1 {
199        return root_seed;
200    }
201    let mut hasher = blake3::Hasher::new();
202    hasher.update(STREAM_DERIVATION_DOMAIN);
203    hasher.update(&root_seed.to_le_bytes());
204    update_text(&mut hasher, &key.namespace);
205    update_text(&mut hasher, &key.name);
206    hasher.update(&key.version.to_le_bytes());
207    let mut seed = [0_u8; 8];
208    seed.copy_from_slice(&hasher.finalize().as_bytes()[..8]);
209    u64::from_le_bytes(seed)
210}
211
212pub(crate) fn core_report_delay_stream() -> RandomStreamKey {
213    RandomStreamKey::new("canwu.core", "knowledge-report-delay", 1)
214}
215
216fn update_text(hasher: &mut blake3::Hasher, value: &str) {
217    let length = u64::try_from(value.len()).unwrap_or(u64::MAX);
218    hasher.update(&length.to_le_bytes());
219    hasher.update(value.as_bytes());
220}