Hi there! This is my first time really dipping into the Rand crate beyond some surface level stuff during AoC. I am making a little game that relies on some tilemaps, and my plan is to support some randomness when defining what possible tile index to choose when drawing a particular position.
My requirements boil down to:
- Weighted values - I want to be able to define the probability of a tile being chosen with a value, rather than inserting it more than once
- Determinism - I want the result to be reproducible across sessions, given some seed.
I think I have achieved both of those in a minimal example (I haven't integrated it into the game yet) but since I only just read the rand book and have never done anything like this before, I'd appreciate any feedback on my current implementation. Especially when it comes to how I am combining the seed and the tile position so that all the tiles aren't the same.
I'd also appreciate any tips regarding performance (I am going to use Pcg64Mcg in the final version), with the context that the random selection will only happen once when each level is loaded and that the game is built on Bevy.
use rand::distributions::WeightedIndex;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub struct TilePos {
x: u32,
y: u32,
}
impl TilePos {
pub fn new(x: u32, y: u32) -> Self {
TilePos { x, y }
}
}
#[derive(Debug)]
pub struct TileMap(Vec<TilePos>);
impl TileMap {
pub fn get_random_value_per_position(
&self,
random_tiles: &WeightedRandomTiles,
seed: u64,
) -> Vec<(TilePos, u32)> {
let mut results = Vec::with_capacity(self.0.len());
for position in &self.0 {
let position_seed = combine_seed_and_tile_pos(seed, position);
let mut rng = ChaCha8Rng::seed_from_u64(position_seed);
let dist = WeightedIndex::new(random_tiles.0.iter().map(|item| item.weight)).unwrap();
let random_index = random_tiles.0[dist.sample(&mut rng)].tile_index;
results.push((*position, random_index));
}
results
}
}
#[derive(Debug)]
pub struct WeightedRandomTiles(Vec<WeightedTileIndex>);
#[derive(Debug, PartialEq)]
pub struct WeightedTileIndex {
tile_index: u32,
weight: u32,
}
impl WeightedTileIndex {
pub fn new(tile_index: u32, weight: u32) -> Self {
WeightedTileIndex { tile_index, weight }
}
}
fn combine_seed_and_tile_pos(seed: u64, position: &TilePos) -> u64 {
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
position.hash(&mut hasher);
hasher.finish()
}
fn main() {
// Represents a 3x3 tilemap
let tile_map = TileMap(vec![
TilePos::new(0, 0),
TilePos::new(1, 0),
TilePos::new(2, 0),
TilePos::new(0, 1),
TilePos::new(1, 1),
TilePos::new(2, 1),
TilePos::new(0, 2),
TilePos::new(1, 2),
TilePos::new(2, 2),
]);
// An abritrary set of random indices where all indices are equally likely
// to be selected, except index 0 which is twice as likely to occur
let random_tiles = WeightedRandomTiles(vec![
WeightedTileIndex::new(0, 2),
WeightedTileIndex::new(1, 1),
WeightedTileIndex::new(2, 1),
WeightedTileIndex::new(3, 1),
]);
// Numeric seed, will probably want to change out for a string later
let seed: u64 = 123456789;
// Get a random weighted random index back from the tilemap
let random_indices = tile_map.get_random_value_per_position(&random_tiles, seed);
// Print all the results of the function out
for (position, selected_index) in &random_indices {
println!(
"Position: ({},{}), Index: {}",
position.x, position.y, selected_index
);
}
// Turn the results into a hashmap, for the sake of indexing
let random_indices: HashMap<TilePos, u32> = random_indices.into_iter().collect();
// Try generating the results 100 times and compare them to the original attempt
for _ in 0..99 {
let random = tile_map.get_random_value_per_position(&random_tiles, seed);
for (position, selected_index) in random {
assert_eq!(*random_indices.get(&position).unwrap(), selected_index);
}
}
}