|
| 1 | +use bevy_app::{App, Plugin}; |
| 2 | +use bevy_utils::tracing::{debug, trace}; |
| 3 | +use rand::{rngs::StdRng, RngCore, SeedableRng}; |
| 4 | + |
| 5 | +pub mod prelude { |
| 6 | + #[doc(hidden)] |
| 7 | + pub use crate::Entropy; |
| 8 | +} |
| 9 | + |
| 10 | +/// Provides a source of entropy. |
| 11 | +/// This enables deterministic random number generation. |
| 12 | +/// |
| 13 | +// See <https://github.com/bevyengine/bevy/discussions/2480> for issues |
| 14 | +// to be mindful of if you desire complete determinism. |
| 15 | +#[derive(Default)] |
| 16 | +pub struct EntropyPlugin; |
| 17 | + |
| 18 | +impl Plugin for EntropyPlugin { |
| 19 | + fn build(&self, app: &mut App) { |
| 20 | + if !app.world.contains_resource::<Entropy>() { |
| 21 | + trace!("Creating entropy"); |
| 22 | + app.init_resource::<Entropy>(); |
| 23 | + } |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +/// A resource that provides entropy. |
| 28 | +pub struct Entropy(StdRng); |
| 29 | + |
| 30 | +impl Default for Entropy { |
| 31 | + /// The default entropy source is non-deterministic and seeded from the operating system. |
| 32 | + /// For a deterministic source, use [`Entropy::from`]. |
| 33 | + fn default() -> Self { |
| 34 | + debug!("Entropy created via the operating system"); |
| 35 | + let rng = StdRng::from_entropy(); |
| 36 | + Entropy(rng) |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +impl Entropy { |
| 41 | + /// Create a deterministic source of entropy. All random number generators |
| 42 | + /// later seeded from an [`Entropy`] created this way will be deterministic. |
| 43 | + /// If determinism is not required, use [`Entropy::default`]. |
| 44 | + pub fn from(seed: [u8; 32]) -> Self { |
| 45 | + debug!("Entropy created via seed: {:?} ", seed); |
| 46 | + let rng = StdRng::from_seed(seed); |
| 47 | + Entropy(rng) |
| 48 | + } |
| 49 | + |
| 50 | + /// Fill `dest` with entropy data. For an allocating alternative, see [`Entropy::get`]. |
| 51 | + pub fn fill_bytes(&mut self, dest: &mut [u8]) { |
| 52 | + self.0.fill_bytes(dest) |
| 53 | + } |
| 54 | + |
| 55 | + /// Allocate and return entropy data. For a non-allocating alternative, see [`Entropy::fill_bytes`]. |
| 56 | + pub fn get(&mut self) -> [u8; 32] { |
| 57 | + let mut dest = [0; 32]; |
| 58 | + self.0.fill_bytes(&mut dest); |
| 59 | + dest |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +#[cfg(test)] |
| 64 | +mod test { |
| 65 | + use bevy_app::AppExit; |
| 66 | + use bevy_ecs::prelude::*; |
| 67 | + use bevy_internal::prelude::*; |
| 68 | + use rand::{rngs::SmallRng, seq::IteratorRandom, SeedableRng}; |
| 69 | + use std::sync::mpsc; |
| 70 | + use std::sync::mpsc::{Receiver, SyncSender}; |
| 71 | + |
| 72 | + #[test] |
| 73 | + fn is_deterministic() { |
| 74 | + const APP_RUN_COUNT: u8 = 10; |
| 75 | + const CHOOSE_COUNT: u8 = 5; |
| 76 | + const THING_COUNT: u8 = 100; |
| 77 | + |
| 78 | + struct Thing(u8); |
| 79 | + struct ResultChannel(SyncSender<u8>); |
| 80 | + |
| 81 | + // The result of the app we will check to make sure it is always the same. |
| 82 | + let mut expected_result: Option<Vec<u8>> = None; |
| 83 | + |
| 84 | + // The seed we will use for the random number generator in all app runs. |
| 85 | + let world_seed: [u8; 32] = [1; 32]; |
| 86 | + |
| 87 | + // Run the app multiple times. |
| 88 | + for runs in 0..APP_RUN_COUNT { |
| 89 | + let (tx, rx): (SyncSender<u8>, Receiver<u8>) = mpsc::sync_channel(CHOOSE_COUNT.into()); |
| 90 | + |
| 91 | + App::new() |
| 92 | + .insert_resource(Entropy::from(world_seed)) |
| 93 | + .insert_resource(ResultChannel(tx)) |
| 94 | + .add_plugins_with(MinimalPlugins, |group| group.add(super::EntropyPlugin)) |
| 95 | + .add_startup_system(spawn_things) |
| 96 | + .add_system(choose_things) |
| 97 | + .run(); |
| 98 | + |
| 99 | + fn spawn_things(mut commands: Commands) { |
| 100 | + for x in 1..THING_COUNT { |
| 101 | + commands.spawn().insert(Thing(x)); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + fn choose_things( |
| 106 | + query: Query<&Thing>, |
| 107 | + mut entropy: ResMut<Entropy>, |
| 108 | + result_channel: Res<ResultChannel>, |
| 109 | + mut app_exit_events: EventWriter<AppExit>, |
| 110 | + ) { |
| 111 | + // Create RNG from global entropy. |
| 112 | + let seed = entropy.get(); |
| 113 | + let mut rng = SmallRng::from_seed(seed); |
| 114 | + |
| 115 | + // Choose some random things. |
| 116 | + for _ in 0..CHOOSE_COUNT { |
| 117 | + if let Some(thing) = query.iter().choose(&mut rng) { |
| 118 | + // Send the chosen thing out of the app so it can be inspected |
| 119 | + // after the app exits. |
| 120 | + result_channel.0.send(thing.0).expect("result to send"); |
| 121 | + } |
| 122 | + } |
| 123 | + app_exit_events.send(AppExit) |
| 124 | + } |
| 125 | + |
| 126 | + // The result of running the app. |
| 127 | + let run_result: Vec<u8> = rx.iter().collect(); |
| 128 | + |
| 129 | + // If it is the first run, treat the current result as the expected |
| 130 | + // result we will check future runs against. |
| 131 | + if runs == 0 { |
| 132 | + expected_result = Some(run_result.clone()); |
| 133 | + } |
| 134 | + |
| 135 | + assert_eq!(expected_result, Some(run_result)); |
| 136 | + } |
| 137 | + } |
| 138 | +} |
0 commit comments