mirror of
https://github.com/italicsjenga/agb.git
synced 2025-01-11 17:41:33 +11:00
Merge pull request #214 from gwilymk/random-number-generator
Random number generator
This commit is contained in:
commit
6d0f95c21b
|
@ -875,7 +875,7 @@ mod test {
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Gba;
|
use crate::{rng::RandomNumberGenerator, Gba};
|
||||||
|
|
||||||
#[test_case]
|
#[test_case]
|
||||||
fn can_store_and_retrieve_8_elements(_gba: &mut Gba) {
|
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 {
|
struct NoisyDrop {
|
||||||
i: i32,
|
i: i32,
|
||||||
dropped: bool,
|
dropped: bool,
|
||||||
|
@ -1034,9 +1005,9 @@ mod test {
|
||||||
let mut answers: [Option<i32>; 128] = [None; 128];
|
let mut answers: [Option<i32>; 128] = [None; 128];
|
||||||
|
|
||||||
for _ in 0..5_000 {
|
for _ in 0..5_000 {
|
||||||
let command = rng.next().rem_euclid(2);
|
let command = rng.gen().rem_euclid(2);
|
||||||
let key = rng.next().rem_euclid(answers.len() as i32);
|
let key = rng.gen().rem_euclid(answers.len() as i32);
|
||||||
let value = rng.next();
|
let value = rng.gen();
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
0 => {
|
0 => {
|
||||||
|
|
|
@ -155,6 +155,8 @@ pub mod mgba;
|
||||||
pub use agb_fixnum as fixnum;
|
pub use agb_fixnum as fixnum;
|
||||||
/// Contains an implementation of a hashmap which suits the gameboy advance's hardware.
|
/// Contains an implementation of a hashmap which suits the gameboy advance's hardware.
|
||||||
pub mod hash_map;
|
pub mod hash_map;
|
||||||
|
/// Simple random number generator
|
||||||
|
pub mod rng;
|
||||||
mod single;
|
mod single;
|
||||||
/// Implements sound output.
|
/// Implements sound output.
|
||||||
pub mod sound;
|
pub mod sound;
|
||||||
|
|
102
agb/src/rng.rs
Normal file
102
agb/src/rng.rs
Normal file
|
@ -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<RefCell<RandomNumberGenerator>> =
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,15 +3,12 @@
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
mod rng;
|
|
||||||
mod sfx;
|
mod sfx;
|
||||||
|
|
||||||
use core::cmp::Ordering;
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
use alloc::{boxed::Box, vec::Vec};
|
use alloc::{boxed::Box, vec::Vec};
|
||||||
|
|
||||||
use rng::get_random;
|
|
||||||
|
|
||||||
use agb::{
|
use agb::{
|
||||||
display::{
|
display::{
|
||||||
object::{Graphics, Object, ObjectController, Sprite, Tag, TagMap},
|
object::{Graphics, Object, ObjectController, Sprite, Tag, TagMap},
|
||||||
|
@ -21,6 +18,7 @@ use agb::{
|
||||||
fixnum::{FixedNum, Rect, Vector2D},
|
fixnum::{FixedNum, Rect, Vector2D},
|
||||||
input::{Button, ButtonController, Tri},
|
input::{Button, ButtonController, Tri},
|
||||||
interrupt::VBlank,
|
interrupt::VBlank,
|
||||||
|
rng,
|
||||||
};
|
};
|
||||||
use generational_arena::Arena;
|
use generational_arena::Arena;
|
||||||
use sfx::Sfx;
|
use sfx::Sfx;
|
||||||
|
@ -1114,7 +1112,7 @@ impl MiniFlameData {
|
||||||
self.sprite_offset = 0;
|
self.sprite_offset = 0;
|
||||||
self.state = MiniFlameState::Dead;
|
self.state = MiniFlameState::Dead;
|
||||||
|
|
||||||
if get_random() % 4 == 0 {
|
if rng::gen() % 4 == 0 {
|
||||||
instruction = UpdateInstruction::CreateParticle(
|
instruction = UpdateInstruction::CreateParticle(
|
||||||
ParticleData::new_health(),
|
ParticleData::new_health(),
|
||||||
entity.position,
|
entity.position,
|
||||||
|
@ -1137,7 +1135,7 @@ impl MiniFlameData {
|
||||||
self.sprite_offset = 0;
|
self.sprite_offset = 0;
|
||||||
self.state = MiniFlameState::Dead;
|
self.state = MiniFlameState::Dead;
|
||||||
|
|
||||||
if get_random() % 4 == 0 {
|
if rng::gen() % 4 == 0 {
|
||||||
instruction = UpdateInstruction::CreateParticle(
|
instruction = UpdateInstruction::CreateParticle(
|
||||||
ParticleData::new_health(),
|
ParticleData::new_health(),
|
||||||
entity.position,
|
entity.position,
|
||||||
|
@ -1700,7 +1698,7 @@ impl<'a> Boss<'a> {
|
||||||
Self {
|
Self {
|
||||||
entity,
|
entity,
|
||||||
health: 5,
|
health: 5,
|
||||||
target_location: get_random().rem_euclid(5) as u8,
|
target_location: rng::gen().rem_euclid(5) as u8,
|
||||||
state: BossActiveState::Damaged(60),
|
state: BossActiveState::Damaged(60),
|
||||||
timer: 0,
|
timer: 0,
|
||||||
screen_coords,
|
screen_coords,
|
||||||
|
@ -1801,9 +1799,9 @@ impl<'a> Boss<'a> {
|
||||||
fn commit(&mut self, offset: Vector2D<Number>) {
|
fn commit(&mut self, offset: Vector2D<Number>) {
|
||||||
let shake = if self.shake_magnitude != 0.into() {
|
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,
|
- 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,
|
- self.shake_magnitude / 2,
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
|
@ -1816,8 +1814,8 @@ impl<'a> Boss<'a> {
|
||||||
}
|
}
|
||||||
fn explode(&self, enemies: &mut Arena<Enemy<'a>>, object_controller: &'a ObjectController) {
|
fn explode(&self, enemies: &mut Arena<Enemy<'a>>, object_controller: &'a ObjectController) {
|
||||||
for _ in 0..(6 - self.health) {
|
for _ in 0..(6 - self.health) {
|
||||||
let x_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(get_random()).rem_euclid(2.into()) - 1;
|
let y_offset: Number = Number::from_raw(rng::gen()).rem_euclid(2.into()) - 1;
|
||||||
let mut flame = Enemy::new(
|
let mut flame = Enemy::new(
|
||||||
object_controller,
|
object_controller,
|
||||||
EnemyData::MiniFlame(MiniFlameData::new()),
|
EnemyData::MiniFlame(MiniFlameData::new()),
|
||||||
|
@ -1830,7 +1828,7 @@ impl<'a> Boss<'a> {
|
||||||
|
|
||||||
fn get_next_target_location(&self) -> u8 {
|
fn get_next_target_location(&self) -> u8 {
|
||||||
loop {
|
loop {
|
||||||
let a = get_random().rem_euclid(5) as u8;
|
let a = rng::gen().rem_euclid(5) as u8;
|
||||||
if a != self.target_location {
|
if a != self.target_location {
|
||||||
break a;
|
break a;
|
||||||
}
|
}
|
||||||
|
@ -1975,8 +1973,8 @@ impl<'a> Game<'a> {
|
||||||
if self.shake_time > 0 {
|
if self.shake_time > 0 {
|
||||||
let size = self.shake_time.min(4) as i32;
|
let size = self.shake_time.min(4) as i32;
|
||||||
let offset: Vector2D<Number> = (
|
let offset: Vector2D<Number> = (
|
||||||
Number::from_raw(get_random()) % size - Number::new(size) / 2,
|
Number::from_raw(rng::gen()) % 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,
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
this_frame_offset += offset;
|
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);
|
game.clear(&mut vram);
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::rng::get_random;
|
|
||||||
use agb::fixnum::Num;
|
use agb::fixnum::Num;
|
||||||
|
use agb::rng;
|
||||||
use agb::sound::mixer::{ChannelId, Mixer, SoundChannel};
|
use agb::sound::mixer::{ChannelId, Mixer, SoundChannel};
|
||||||
|
|
||||||
const BAT_DEATH: &[u8] = agb::include_wav!("sfx/BatDeath.wav");
|
const BAT_DEATH: &[u8] = agb::include_wav!("sfx/BatDeath.wav");
|
||||||
|
@ -85,7 +85,7 @@ impl<'a> Sfx<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jump(&mut self) {
|
pub fn jump(&mut self) {
|
||||||
let r = get_random() % 3;
|
let r = rng::gen() % 3;
|
||||||
|
|
||||||
let channel = match r {
|
let channel = match r {
|
||||||
0 => SoundChannel::new(JUMP1),
|
0 => SoundChannel::new(JUMP1),
|
||||||
|
|
Loading…
Reference in a new issue