From 9752377a15e14f76ffc4c3324784b43cfca63dbf Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Wed, 23 Mar 2022 21:46:59 +0000 Subject: [PATCH 1/6] Move the random number generator to a crate level thing --- agb/src/hash_map.rs | 31 +------------------------------ agb/src/lib.rs | 2 ++ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 3a53868e..fe9ee3a6 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -918,35 +918,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, @@ -985,7 +956,7 @@ mod test { #[test_case] fn extreme_case(_gba: &mut Gba) { let mut map = HashMap::new(); - let mut rng = RandomNumberGenerator::new(); + let mut rng = crate::rng::RandomNumberGenerator::new(); let mut answers: [Option; 128] = [None; 128]; diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 245bc690..66d8c2b8 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -153,6 +153,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; From 610722a1bfd91e0ef527d5f593a60228d31cf2ed Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Wed, 23 Mar 2022 21:52:37 +0000 Subject: [PATCH 2/6] Extract the random number generator --- agb/src/hash_map.rs | 9 ++++----- agb/src/rng.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 agb/src/rng.rs diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index fe9ee3a6..90094785 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -831,7 +831,7 @@ mod test { use core::cell::RefCell; use super::*; - use crate::Gba; + use crate::{rng, Gba}; #[test_case] fn can_store_and_retrieve_8_elements(_gba: &mut Gba) { @@ -956,14 +956,13 @@ mod test { #[test_case] fn extreme_case(_gba: &mut Gba) { let mut map = HashMap::new(); - let mut rng = crate::rng::RandomNumberGenerator::new(); 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::next().rem_euclid(2); + let key = rng::next().rem_euclid(answers.len() as i32); + let value = rng::next(); match command { 0 => { diff --git a/agb/src/rng.rs b/agb/src/rng.rs new file mode 100644 index 00000000..57bc4405 --- /dev/null +++ b/agb/src/rng.rs @@ -0,0 +1,41 @@ +use core::cell::RefCell; + +use bare_metal::Mutex; + +use crate::interrupt::free; + +pub struct RandomNumberGenerator { + state: [u32; 4], +} + +impl RandomNumberGenerator { + pub const fn new() -> Self { + Self { + state: [1014776995, 476057059, 3301633994, 706340607], + } + } + + pub 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 GLOBAL_RNG: Mutex> = + Mutex::new(RefCell::new(RandomNumberGenerator::new())); + +pub fn next() -> i32 { + free(|cs| GLOBAL_RNG.borrow(*cs).borrow_mut().next()) +} From 3c52f6940b1eaf9c0ab1c5cf357dda68aa612135 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Wed, 23 Mar 2022 21:53:09 +0000 Subject: [PATCH 3/6] Add new_with_seed method --- agb/src/rng.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agb/src/rng.rs b/agb/src/rng.rs index 57bc4405..411f8976 100644 --- a/agb/src/rng.rs +++ b/agb/src/rng.rs @@ -15,6 +15,10 @@ impl RandomNumberGenerator { } } + pub const fn new_with_seed(seed: [u32; 4]) -> Self { + Self { state: seed } + } + pub fn next(&mut self) -> i32 { let result = (self.state[0].wrapping_add(self.state[3])) .rotate_left(7) From a57043604d0048483b440b0d2ff4fa91371e895a Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Wed, 23 Mar 2022 22:05:41 +0000 Subject: [PATCH 4/6] Add some tests for the rng --- agb/src/hash_map.rs | 9 ++++--- agb/src/rng.rs | 63 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index 90094785..e6e98532 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -831,7 +831,7 @@ mod test { use core::cell::RefCell; use super::*; - use crate::{rng, Gba}; + use crate::{rng::RandomNumberGenerator, Gba}; #[test_case] fn can_store_and_retrieve_8_elements(_gba: &mut Gba) { @@ -956,13 +956,14 @@ mod test { #[test_case] fn extreme_case(_gba: &mut Gba) { let mut map = HashMap::new(); + let mut rng = RandomNumberGenerator::new(); 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.next().rem_euclid(2); + let key = rng.next().rem_euclid(answers.len() as i32); + let value = rng.next(); match command { 0 => { diff --git a/agb/src/rng.rs b/agb/src/rng.rs index 411f8976..bbbc2ae3 100644 --- a/agb/src/rng.rs +++ b/agb/src/rng.rs @@ -4,21 +4,34 @@ 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 { - state: [1014776995, 476057059, 3301633994, 706340607], - } + 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 next(&mut self) -> i32 { let result = (self.state[0].wrapping_add(self.state[3])) .rotate_left(7) @@ -40,6 +53,50 @@ impl RandomNumberGenerator { static GLOBAL_RNG: Mutex> = Mutex::new(RefCell::new(RandomNumberGenerator::new())); +/// Using a global random number generator, provides the next random number pub fn next() -> i32 { free(|cs| GLOBAL_RNG.borrow(*cs).borrow_mut().next()) } + +#[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.next().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::next().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 + ); + } + } +} From 7ad52a2974be3c7c16030503709771309e9e5345 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Wed, 23 Mar 2022 22:08:53 +0000 Subject: [PATCH 5/6] Use agb's random rather than own implementation --- examples/the-purple-night/src/main.rs | 26 ++++++++++---------- examples/the-purple-night/src/rng.rs | 34 --------------------------- examples/the-purple-night/src/sfx.rs | 4 ++-- 3 files changed, 14 insertions(+), 50 deletions(-) delete mode 100644 examples/the-purple-night/src/rng.rs diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index 6ab5aa03..e4e8d516 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; @@ -1117,7 +1115,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if get_random() % 4 == 0 { + if rng::next() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1140,7 +1138,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if get_random() % 4 == 0 { + if rng::next() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1705,7 +1703,7 @@ impl<'a> Boss<'a> { Self { entity, health: 5, - target_location: get_random().rem_euclid(5) as u8, + target_location: rng::next().rem_euclid(5) as u8, state: BossActiveState::Damaged(60), timer: 0, screen_coords, @@ -1806,9 +1804,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::next()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, - Number::from_raw(get_random()).rem_euclid(self.shake_magnitude) + Number::from_raw(rng::next()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, ) .into() @@ -1821,8 +1819,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::next()).rem_euclid(2.into()) - 1; + let y_offset: Number = Number::from_raw(rng::next()).rem_euclid(2.into()) - 1; let mut flame = Enemy::new( object_controller, EnemyData::MiniFlame(MiniFlameData::new()), @@ -1835,7 +1833,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::next().rem_euclid(5) as u8; if a != self.target_location { break a; } @@ -1980,8 +1978,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::next()) % size - Number::new(size) / 2, + Number::from_raw(rng::next()) % size - Number::new(size) / 2, ) .into(); this_frame_offset += offset; @@ -2298,7 +2296,7 @@ fn game_with_level(gba: &mut agb::Gba) { } } - get_random(); // advance RNG to make it less predictable between runs + rng::next(); // 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..544e09c7 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::next() % 3; let channel = match r { 0 => SoundChannel::new(JUMP1), From dc62ba44675b8ece88adbf995d0e0f2ca460dfb8 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Mon, 11 Apr 2022 22:07:31 +0100 Subject: [PATCH 6/6] Rename rng::next to rng::gen --- agb/src/hash_map.rs | 6 +++--- agb/src/rng.rs | 10 +++++----- examples/the-purple-night/src/main.rs | 22 +++++++++++----------- examples/the-purple-night/src/sfx.rs | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/agb/src/hash_map.rs b/agb/src/hash_map.rs index e6e98532..56bfa062 100644 --- a/agb/src/hash_map.rs +++ b/agb/src/hash_map.rs @@ -961,9 +961,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/rng.rs b/agb/src/rng.rs index bbbc2ae3..8d73a819 100644 --- a/agb/src/rng.rs +++ b/agb/src/rng.rs @@ -32,7 +32,7 @@ impl RandomNumberGenerator { } /// Returns the next value for the random number generator - pub fn next(&mut self) -> i32 { + pub fn gen(&mut self) -> i32 { let result = (self.state[0].wrapping_add(self.state[3])) .rotate_left(7) .wrapping_mul(9); @@ -54,8 +54,8 @@ static GLOBAL_RNG: Mutex> = Mutex::new(RefCell::new(RandomNumberGenerator::new())); /// Using a global random number generator, provides the next random number -pub fn next() -> i32 { - free(|cs| GLOBAL_RNG.borrow(*cs).borrow_mut().next()) +pub fn gen() -> i32 { + free(|cs| GLOBAL_RNG.borrow(*cs).borrow_mut().gen()) } #[cfg(test)] @@ -69,7 +69,7 @@ mod test { let mut rng = RandomNumberGenerator::new(); for _ in 0..500 { - values[(rng.next().rem_euclid(16)) as usize] += 1; + values[(rng.gen().rem_euclid(16)) as usize] += 1; } for (i, &value) in values.iter().enumerate() { @@ -87,7 +87,7 @@ mod test { let mut values: [u32; 16] = Default::default(); for _ in 0..500 { - values[super::next().rem_euclid(16) as usize] += 1; + values[super::gen().rem_euclid(16) as usize] += 1; } for (i, &value) in values.iter().enumerate() { diff --git a/examples/the-purple-night/src/main.rs b/examples/the-purple-night/src/main.rs index e4e8d516..388d02d6 100644 --- a/examples/the-purple-night/src/main.rs +++ b/examples/the-purple-night/src/main.rs @@ -1115,7 +1115,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if rng::next() % 4 == 0 { + if rng::gen() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1138,7 +1138,7 @@ impl MiniFlameData { self.sprite_offset = 0; self.state = MiniFlameState::Dead; - if rng::next() % 4 == 0 { + if rng::gen() % 4 == 0 { instruction = UpdateInstruction::CreateParticle( ParticleData::new_health(), entity.position, @@ -1703,7 +1703,7 @@ impl<'a> Boss<'a> { Self { entity, health: 5, - target_location: rng::next().rem_euclid(5) as u8, + target_location: rng::gen().rem_euclid(5) as u8, state: BossActiveState::Damaged(60), timer: 0, screen_coords, @@ -1804,9 +1804,9 @@ impl<'a> Boss<'a> { fn commit(&mut self, offset: Vector2D) { let shake = if self.shake_magnitude != 0.into() { ( - Number::from_raw(rng::next()).rem_euclid(self.shake_magnitude) + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, - Number::from_raw(rng::next()).rem_euclid(self.shake_magnitude) + Number::from_raw(rng::gen()).rem_euclid(self.shake_magnitude) - self.shake_magnitude / 2, ) .into() @@ -1819,8 +1819,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(rng::next()).rem_euclid(2.into()) - 1; - let y_offset: Number = Number::from_raw(rng::next()).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()), @@ -1833,7 +1833,7 @@ impl<'a> Boss<'a> { fn get_next_target_location(&self) -> u8 { loop { - let a = rng::next().rem_euclid(5) as u8; + let a = rng::gen().rem_euclid(5) as u8; if a != self.target_location { break a; } @@ -1978,8 +1978,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(rng::next()) % size - Number::new(size) / 2, - Number::from_raw(rng::next()) % 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; @@ -2296,7 +2296,7 @@ fn game_with_level(gba: &mut agb::Gba) { } } - rng::next(); // 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/sfx.rs b/examples/the-purple-night/src/sfx.rs index 544e09c7..d7f6d089 100644 --- a/examples/the-purple-night/src/sfx.rs +++ b/examples/the-purple-night/src/sfx.rs @@ -85,7 +85,7 @@ impl<'a> Sfx<'a> { } pub fn jump(&mut self) { - let r = rng::next() % 3; + let r = rng::gen() % 3; let channel = match r { 0 => SoundChannel::new(JUMP1),