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}