diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index be75786a..1f321aac 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -875,7 +875,7 @@ mod test { use core::cell::RefCell; use super::*; - use crate::Gba; + use crate::{rng::RandomNumberGenerator, Gba}; #[test_case] fn can_store_and_retrieve_8_elements(_gba: &mut Gba) { @@ -962,35 +962,6 @@ mod test { } } - struct RandomNumberGenerator { - state: [u32; 4], - } - - impl RandomNumberGenerator { - const fn new() -> Self { - Self { - state: [1014776995, 476057059, 3301633994, 706340607], - } - } - - fn next(&mut self) -> i32 { - let result = (self.state[0].wrapping_add(self.state[3])) - .rotate_left(7) - .wrapping_mul(9); - let t = self.state[1].wrapping_shr(9); - - self.state[2] ^= self.state[0]; - self.state[3] ^= self.state[1]; - self.state[1] ^= self.state[2]; - self.state[0] ^= self.state[3]; - - self.state[2] ^= t; - self.state[3] = self.state[3].rotate_left(11); - - result as i32 - } - } - struct NoisyDrop { i: i32, dropped: bool, @@ -1034,9 +1005,9 @@ mod test { let mut answers: [Option; 128] = [None; 128]; for _ in 0..5_000 { - let command = rng.next().rem_euclid(2); - let key = rng.next().rem_euclid(answers.len() as i32); - let value = rng.next(); + let command = rng.gen().rem_euclid(2); + let key = rng.gen().rem_euclid(answers.len() as i32); + let value = rng.gen(); match command { 0 => { diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 6fcd1bb9..2ea42ddb 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -155,6 +155,8 @@ pub mod mgba; pub use agb_fixnum as fixnum; /// Contains an implementation of a hashmap which suits the gameboy advance's hardware. pub mod hash_map; +/// Simple random number generator +pub mod rng; mod single; /// Implements sound output. pub mod sound; diff --git a/agb/src/rng.rs b/agb/src/rng.rs new file mode 100644 index 00000000..8d73a819 --- /dev/null +++ b/agb/src/rng.rs @@ -0,0 +1,102 @@ +use core::cell::RefCell; + +use bare_metal::Mutex; + +use crate::interrupt::free; + +/// A fast pseudo-random number generator. Note that the output of the +/// random number generator for a given seed is guaranteed stable +/// between minor releases, however could change in a major release. +pub struct RandomNumberGenerator { + state: [u32; 4], +} + +impl RandomNumberGenerator { + /// Create a new random number generator with a fixed seed + /// + /// Note that this seed is guaranteed to be the same between minor releases. + pub const fn new() -> Self { + Self::new_with_seed([1014776995, 476057059, 3301633994, 706340607]) + } + + /// Produces a random number generator with the given initial state / seed. + /// None of the values can be 0. + pub const fn new_with_seed(seed: [u32; 4]) -> Self { + // this can't be in a loop because const + assert!(seed[0] != 0, "seed must not be 0"); + assert!(seed[1] != 0, "seed must not be 0"); + assert!(seed[2] != 0, "seed must not be 0"); + assert!(seed[3] != 0, "seed must not be 0"); + + Self { state: seed } + } + + /// Returns the next value for the random number generator + pub fn gen(&mut self) -> i32 { + let result = (self.state[0].wrapping_add(self.state[3])) + .rotate_left(7) + .wrapping_mul(9); + let t = self.state[1].wrapping_shr(9); + + self.state[2] ^= self.state[0]; + self.state[3] ^= self.state[1]; + self.state[1] ^= self.state[2]; + self.state[0] ^= self.state[3]; + + self.state[2] ^= t; + self.state[3] = self.state[3].rotate_left(11); + + result as i32 + } +} + +static GLOBAL_RNG: Mutex> = + Mutex::new(RefCell::new(RandomNumberGenerator::new())); + +/// Using a global random number generator, provides the next random number +pub fn gen() -> i32 { + free(|cs| GLOBAL_RNG.borrow(*cs).borrow_mut().gen()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Gba; + + #[test_case] + fn should_be_reasonably_distributed(_gba: &mut Gba) { + let mut values: [u32; 16] = Default::default(); + + let mut rng = RandomNumberGenerator::new(); + for _ in 0..500 { + values[(rng.gen().rem_euclid(16)) as usize] += 1; + } + + for (i, &value) in values.iter().enumerate() { + assert!( + value >= 500 / 10 / 3, + "{} came up less than expected {}", + i, + value + ); + } + } + + #[test_case] + fn global_rng_should_be_reasonably_distributed(_gba: &mut Gba) { + let mut values: [u32; 16] = Default::default(); + + for _ in 0..500 { + values[super::gen().rem_euclid(16) as usize] += 1; + } + + for (i, &value) in values.iter().enumerate() { + assert!( + value >= 500 / 10 / 3, + "{} came up less than expected {}", + i, + value + ); + } + } +} diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index fa0dc996..ea822c4d 100644 --- a/examples/the-purple-night/src/main.rs +++ b/examples/the-purple-night/src/main.rs @@ -3,15 +3,12 @@ extern crate alloc; -mod rng; mod sfx; use core::cmp::Ordering; use alloc::{boxed::Box, vec::Vec}; -use rng::get_random; - use agb::{ display::{ object::{Graphics, Object, ObjectController, Sprite, Tag, TagMap}, @@ -21,6 +18,7 @@ use agb::{ fixnum::{FixedNum, Rect, Vector2D}, input::{Button, ButtonController, Tri}, interrupt::VBlank, + rng, }; use generational_arena::Arena; use sfx::Sfx; @@ -1114,7 +1112,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if get_random() % 4 == 0 { + if rng::gen() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1137,7 +1135,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if get_random() % 4 == 0 { + if rng::gen() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1700,7 +1698,7 @@ impl<'a> Boss<'a> { Self { entity, health: 5, - target_location: get_random().rem_euclid(5) as u8, + target_location: rng::gen().rem_euclid(5) as u8, state: BossActiveState::Damaged(60), timer: 0, screen_coords, @@ -1801,9 +1799,9 @@ impl<'a> Boss<'a> { fn commit(&mut self, offset: Vector2D) { let shake = if self.shake_magnitude != 0.into() { ( - Number::from_raw(get_random()).rem_euclid(self.shake_magnitude) + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, - Number::from_raw(get_random()).rem_euclid(self.shake_magnitude) + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, ) .into() @@ -1816,8 +1814,8 @@ impl<'a> Boss<'a> { } fn explode(&self, enemies: &mut Arena>, object_controller: &'a ObjectController) { for _ in 0..(6 - self.health) { - let x_offset: Number = Number::from_raw(get_random()).rem_euclid(2.into()) - 1; - let y_offset: Number = Number::from_raw(get_random()).rem_euclid(2.into()) - 1; + let x_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; + let y_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1; let mut flame = Enemy::new( object_controller, EnemyData::MiniFlame(MiniFlameData::new()), @@ -1830,7 +1828,7 @@ impl<'a> Boss<'a> { fn get_next_target_location(&self) -> u8 { loop { - let a = get_random().rem_euclid(5) as u8; + let a = rng::gen().rem_euclid(5) as u8; if a != self.target_location { break a; } @@ -1975,8 +1973,8 @@ impl<'a> Game<'a> { if self.shake_time > 0 { let size = self.shake_time.min(4) as i32; let offset: Vector2D = ( - Number::from_raw(get_random()) % size - Number::new(size) / 2, - Number::from_raw(get_random()) % size - Number::new(size) / 2, + Number::from_raw(rng::gen()) % size - Number::new(size) / 2, + Number::from_raw(rng::gen()) % size - Number::new(size) / 2, ) .into(); this_frame_offset += offset; @@ -2291,7 +2289,7 @@ fn game_with_level(gba: &mut agb::Gba) { } } - get_random(); // advance RNG to make it less predictable between runs + rng::gen(); // advance RNG to make it less predictable between runs }; game.clear(&mut vram); diff --git a/examples/the-purple-night/src/rng.rs b/examples/the-purple-night/src/rng.rs deleted file mode 100644 index 8666243c..00000000 --- a/examples/the-purple-night/src/rng.rs +++ /dev/null @@ -1,34 +0,0 @@ -struct RandomNumberGenerator { - state: [u32; 4], -} - -impl RandomNumberGenerator { - const fn new() -> Self { - Self { - state: [1014776995, 476057059, 3301633994, 706340607], - } - } - - fn next(&mut self) -> i32 { - let result = (self.state[0].wrapping_add(self.state[3])) - .rotate_left(7) - .wrapping_mul(9); - let t = self.state[1].wrapping_shr(9); - - self.state[2] ^= self.state[0]; - self.state[3] ^= self.state[1]; - self.state[1] ^= self.state[2]; - self.state[0] ^= self.state[3]; - - self.state[2] ^= t; - self.state[3] = self.state[3].rotate_left(11); - - result as i32 - } -} - -static mut RANDOM_GENERATOR: RandomNumberGenerator = RandomNumberGenerator::new(); - -pub fn get_random() -> i32 { - unsafe { &mut RANDOM_GENERATOR }.next() -} diff --git a/examples/the-purple-night/src/sfx.rs b/examples/the-purple-night/src/sfx.rs index 1164413e..d7f6d089 100644 --- a/examples/the-purple-night/src/sfx.rs +++ b/examples/the-purple-night/src/sfx.rs @@ -1,5 +1,5 @@ -use super::rng::get_random; use agb::fixnum::Num; +use agb::rng; use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; const BAT_DEATH: &[u8] = agb::include_wav!("sfx/BatDeath.wav"); @@ -85,7 +85,7 @@ impl<'a> Sfx<'a> { } pub fn jump(&mut self) { - let r = get_random() % 3; + let r = rng::gen() % 3; let channel = match r { 0 => SoundChannel::new(JUMP1),