Skip to content

Commit

Permalink
improve rng generator determinism
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksa2808 committed Dec 13, 2023
1 parent 0d29a5c commit 5e78421
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 41 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
rand = "0.8"
rand_xoshiro = "0.6"
itertools = "0.10"
bevy_ggrs = { version = "0.14.0" }
bytemuck = { version = "1.7", features = ["derive"] }
Expand Down Expand Up @@ -50,5 +51,6 @@ parking_lot = "0.12"
wasm-bindgen = "0.2"

[patch.crates-io]
# bevy_ggrs = { path = "../bevy_ggrs" }
bevy_ggrs = { git = "https://github.com/aleksa2808/bevy_ggrs" }
ggrs = { git = "https://github.com/aleksa2808/ggrs" }
4 changes: 2 additions & 2 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ pub const BOMB_SHORTENED_FUSE_FRAME_COUNT: usize = 2;

pub const MOVING_OBJECT_FRAME_INTERVAL: usize = 1;

// TODO does float precision cause desyncs?
pub const ITEM_SPAWN_CHANCE_PERCENTAGE: usize = 33;
// TODO figure out if floats can be used deterministically
pub const ITEM_SPAWN_CHANCE_PERCENTAGE: u64 = 33;
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub fn run() {
.rollback_resource_with_copy::<FrameCount>()
.rollback_resource_with_copy::<WallOfDeath>()
.rollback_resource_with_copy::<GameFreeze>()
// checksums
.checksum_component_with_hash::<Player>()
.checksum_component_with_hash::<Position>()
.checksum_component_with_hash::<BombSatchel>()
Expand Down
31 changes: 22 additions & 9 deletions src/resources.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::{ecs as bevy_ecs, prelude::*, text::Font, utils::HashMap};
use bevy_matchbox::matchbox_socket::PeerId;
use rand::{rngs::StdRng, seq::IteratorRandom, Rng};
use rand::{Rng, SeedableRng};
use rand_xoshiro::Xoshiro256StarStar;

use crate::{
components::Position,
Expand Down Expand Up @@ -164,20 +165,20 @@ pub enum WorldType {
impl WorldType {
pub const LIST: [Self; 3] = [Self::GrassWorld, Self::IceWorld, Self::CloudWorld];

pub fn random(rng: &mut StdRng) -> Self {
match rng.gen_range(1..=3) {
1 => Self::GrassWorld,
2 => Self::IceWorld,
3 => Self::CloudWorld,
pub fn random(rng: &mut SessionRng) -> Self {
match rng.gen_u64() % 3 {
0 => Self::GrassWorld,
1 => Self::IceWorld,
2 => Self::CloudWorld,
_ => unreachable!(),
}
}

pub fn next_random(&self, rng: &mut StdRng) -> Self {
pub fn next_random(&self, rng: &mut SessionRng) -> Self {
Self::LIST
.into_iter()
.filter(|&w| w != *self)
.choose(rng)
.nth((rng.gen_u64() as usize) % (Self::LIST.len() - 1))
.unwrap()
}
}
Expand All @@ -196,8 +197,20 @@ pub struct RngSeeds {
pub remote: HashMap<PeerId, Option<u64>>,
}

// I could not verify it but I assume that the Xoshiro256StarStar generator is platform-independent. This is necessary for cross-platform deterministic gameplay.
#[derive(Resource, Clone)]
pub struct SessionRng(pub StdRng);
pub struct SessionRng(Xoshiro256StarStar);

impl SessionRng {
pub fn new(seed: u64) -> Self {
Self(Xoshiro256StarStar::seed_from_u64(seed))
}

// Allow only `u64` number generation in order to prevent things like generating platform dependent `usize` values.
pub fn gen_u64(&mut self) -> u64 {
self.0.gen()
}
}

#[derive(Resource)]
pub struct LocalPlayerID(pub usize);
Expand Down
35 changes: 16 additions & 19 deletions src/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ use bevy_matchbox::{
MatchboxSocket,
};
use itertools::Itertools;
use rand::{
rngs::StdRng,
seq::{IteratorRandom, SliceRandom},
Rng, SeedableRng,
};

use crate::{
components::*,
Expand All @@ -33,7 +28,7 @@ use crate::{
utils::{
burn_item, decode, format_hud_time, generate_item_at_position, get_x, get_y,
setup_fullscreen_message_display, setup_get_ready_display, setup_leaderboard_display,
setup_round, setup_tournament_winner_display,
setup_round, setup_tournament_winner_display, shuffle,
},
AppState, GgrsConfig,
};
Expand Down Expand Up @@ -249,7 +244,7 @@ pub fn lobby_system(
rng_seeds.local ^ peer_rng_seeds.into_iter().reduce(|acc, e| acc ^ e).unwrap();
info!("Generated the shared RNG seed: {shared_seed}");
commands.remove_resource::<RngSeeds>();
commands.insert_resource(SessionRng(StdRng::seed_from_u64(shared_seed)));
commands.insert_resource(SessionRng::new(shared_seed));

// extract final player list
let players = socket.players();
Expand Down Expand Up @@ -344,7 +339,7 @@ pub fn setup_game(
local_player_id: Res<LocalPlayerID>,
) {
// choose the initial world
let world_type = WorldType::random(&mut session_rng.0);
let world_type = WorldType::random(&mut session_rng);
commands.insert_resource(world_type);

// setup the tournament leaderboard
Expand Down Expand Up @@ -454,7 +449,7 @@ pub fn player_move(
.sorted_by_cached_key(|q| rollback_ordered.order(*q.0))
.collect_vec();
// shuffle to ensure fairness in situations where two players push the same bomb in the same frame
players.shuffle(&mut session_rng.0);
shuffle(&mut players, &mut session_rng);
for (_, player, mut position, mut transform, mut sprite) in players {
let input = inputs[player.id.0].0 .0;
for (input_mask, moving_direction) in [
Expand Down Expand Up @@ -678,7 +673,7 @@ pub fn bomb_drop(
.sorted_by_cached_key(|q| rollback_ordered.order(*q.0))
.collect_vec();
// shuffle to ensure fairness in situations where two players try to place a bomb in the same frame
players.shuffle(&mut session_rng.0);
shuffle(&mut players, &mut session_rng);
for (_, player, position, mut bomb_satchel) in players {
if inputs[player.id.0].0 .0 & INPUT_ACTION != 0
&& bomb_satchel.bombs_available > 0
Expand Down Expand Up @@ -894,9 +889,9 @@ pub fn crumbling_tick(
commands.entity(entity).despawn_recursive();

// drop power-up
let roll = session_rng.0.gen_range(0..100);
let roll = session_rng.gen_u64() % 100;
if roll < ITEM_SPAWN_CHANCE_PERCENTAGE {
generate_item_at_position(&mut session_rng.0, &mut commands, &game_textures, *position);
generate_item_at_position(&mut session_rng, &mut commands, &game_textures, *position);
}
}
}
Expand Down Expand Up @@ -1328,17 +1323,19 @@ pub fn cleanup_dead(
// death pinata
let invalid_item_positions: HashSet<Position> =
invalid_item_position_query.iter().copied().collect();
let valid_positions = (1..map_size.rows - 1)
let mut valid_positions = (1..map_size.rows - 1)
.flat_map(|y| {
(1..map_size.columns - 1).map(move |x| Position {
y: y as isize,
x: x as isize,
})
})
.filter(|position| !invalid_item_positions.contains(position));
for position in valid_positions.choose_multiple(&mut session_rng.0, 3) {
.filter(|position| !invalid_item_positions.contains(position))
.collect_vec();
shuffle(&mut valid_positions, &mut session_rng);
for &position in valid_positions.iter().take(3) {
generate_item_at_position(
&mut session_rng.0,
&mut session_rng,
&mut commands,
&game_textures,
position,
Expand Down Expand Up @@ -1435,7 +1432,7 @@ pub fn show_leaderboard(
let window = primary_window_query.get_single().unwrap();

setup_leaderboard_display(
&mut session_rng.0,
&mut session_rng,
parent,
window.height(),
window.width(),
Expand Down Expand Up @@ -1498,7 +1495,7 @@ pub fn show_tournament_winner(
}

// choose a world for the next tournament
*world_type = world_type.next_random(&mut session_rng.0);
*world_type = world_type.next_random(&mut session_rng);

commands.insert_resource(GameFreeze {
end_frame: frame_count.frame + TOURNAMENT_WINNER_DISPLAY_FRAME_COUNT,
Expand Down Expand Up @@ -1534,7 +1531,7 @@ pub fn start_new_round(

let round_start_frame = frame_count.frame + GAME_START_FREEZE_FRAME_COUNT;
setup_round(
&mut session_rng.0,
&mut session_rng,
&mut commands,
*map_size,
*world_type,
Expand Down
37 changes: 26 additions & 11 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use bevy::{
};
use bevy_ggrs::AddRollbackCommandExtension;
use itertools::Itertools;
use rand::{rngs::StdRng, seq::IteratorRandom, Rng};

use crate::{
components::{
Expand All @@ -25,7 +24,8 @@ use crate::{
PLAYER_Z_LAYER, ROUND_DURATION_SECS, TILE_HEIGHT, TILE_WIDTH, WALL_Z_LAYER,
},
resources::{
Fonts, GameEndFrame, GameTextures, HUDColors, Leaderboard, MapSize, WallOfDeath, WorldType,
Fonts, GameEndFrame, GameTextures, HUDColors, Leaderboard, MapSize, SessionRng,
WallOfDeath, WorldType,
},
types::{Direction, PlayerID, RoundOutcome},
};
Expand All @@ -42,6 +42,12 @@ pub fn decode(input: &str) -> String {
String::from_utf8(STANDARD_NO_PAD.decode(input).unwrap()).unwrap()
}

pub fn shuffle<T>(elements: &mut [T], rng: &mut SessionRng) {
for i in (1..elements.len()).rev() {
elements.swap(i, rng.gen_u64() as usize % (i + 1));
}
}

pub fn setup_fullscreen_message_display(
commands: &mut Commands,
window: &Window,
Expand Down Expand Up @@ -387,7 +393,7 @@ fn init_hud(
}

fn spawn_map(
rng: &mut StdRng,
rng: &mut SessionRng,
commands: &mut Commands,
game_textures: &GameTextures,
world_type: WorldType,
Expand Down Expand Up @@ -503,11 +509,16 @@ fn spawn_map(
);
}

let destructible_wall_positions = destructible_wall_potential_positions
let mut destructible_wall_positions = destructible_wall_potential_positions
.into_iter()
.sorted_by_key(|p| (p.x, p.y))
.choose_multiple(rng, num_of_destructible_walls_to_place);
for position in destructible_wall_positions.iter().cloned() {
.collect_vec();
shuffle(&mut destructible_wall_positions, rng);
for position in destructible_wall_positions
.iter()
.take(num_of_destructible_walls_to_place)
.cloned()
{
commands
.spawn((
SpriteBundle {
Expand Down Expand Up @@ -536,7 +547,7 @@ fn spawn_map(
}

pub fn setup_round(
rng: &mut StdRng,
rng: &mut SessionRng,
commands: &mut Commands,
map_size: MapSize,
world_type: WorldType,
Expand Down Expand Up @@ -646,12 +657,12 @@ pub fn setup_round(
}

pub fn generate_item_at_position(
rng: &mut StdRng,
rng: &mut SessionRng,
commands: &mut Commands,
game_textures: &GameTextures,
position: Position,
) {
let roll = rng.gen::<usize>() % 100;
let roll = rng.gen_u64() % 100;

/* "Loot tables" */
let item = match roll {
Expand Down Expand Up @@ -699,7 +710,7 @@ pub fn burn_item(
}

pub fn setup_leaderboard_display(
rng: &mut StdRng,
rng: &mut SessionRng,
parent: &mut ChildBuilder,
window_height: f32,
window_width: f32,
Expand Down Expand Up @@ -845,7 +856,11 @@ pub fn setup_leaderboard_display(
height: Val::Px(PIXEL_SCALE as f32),
..Default::default()
},
background_color: (*COLORS.iter().choose(rng).unwrap()).into(),
background_color: (*COLORS
.iter()
.nth(rng.gen_u64() as usize % COLORS.len())
.unwrap())
.into(),
..Default::default()
},
UIComponent,
Expand Down

0 comments on commit 5e78421

Please sign in to comment.