Deterministic RNG utility for the game. It maintains three independent System.Random streams (authoritative, cosmetic, background), provides seeding and methods to get ints and floats for each stream, and documents why streams are separate.
using System;
namespace BlockParty;
/// <summary>
/// Deterministic random source for the simulation. The original game used
/// GameAPI's <c>Mathf.Random</c>; we route everything through a single seeded
/// instance so a given seed reproduces an identical run.
///
/// THREE independent streams:
/// - The AUTHORITATIVE stream (<see cref="Int"/>/<see cref="Float"/>/<see cref="Value"/>) drives
/// everything that affects gameplay state: block spawn positions, block move-direction picks,
/// block-triggered effects, spikes, laser angles, etc. This stream is a deterministic function of
/// (seed + the complete input history) — NOT of (seed, step) alone: the player perturbs it by
/// pressing sides, which upgrades a block's phase, and phase changes both its motion (so it hits
/// walls/blocks at different steps, shifting when direction re-picks draw) and unlocks its
/// phase-gated attacks (which draw laser angles, spike/teardrop placement, etc.). So only the
/// INITIAL layout (spawn positions/types, drawn before any input) is identical for every player on
/// a given seed; everything after the first tick diverges with how the run is played. A daily
/// challenge shares only that starting layout — the rest of a run is reproduced not from the seed
/// but from the recorded INPUT (an input-replay feeds that back, reproducing the exact draw
/// sequence).
/// - The COSMETIC stream (<see cref="CosmeticInt"/>/<see cref="CosmeticFloat"/>/<see cref="CosmeticValue"/>)
/// drives PLAYER-TRIGGERED visual-only effects (dust, blood, death particles). These fire in
/// response to player input; drawing them from the authoritative stream would let INCIDENTAL
/// visuals perturb every subsequent block decision, so they're routed here where they can't touch
/// the sim. (Note this only isolates those side effects — a deliberate side-press still affects the
/// sim through the authoritative stream above; that coupling is the game mechanic, by design.)
/// - The BACKGROUND stream (<see cref="BackgroundInt"/>/<see cref="BackgroundFloat"/>/
/// <see cref="BackgroundValue"/>) drives autonomous decorative background blocks. It is separate
/// from both gameplay and event-driven particles, so changing level ambience cannot alter either.
///
/// Semantics match the original:
/// - <see cref="Int"/> returns a value in [min, max) (upper-exclusive, like System.Random.Next).
/// - <see cref="Float"/> returns a value in [min, max).
/// </summary>
public static class Rng
{
private static Random _random = new Random(); // authoritative sim stream
private static Random _cosmetic = new Random(); // player-triggered, cosmetic-only stream
private static Random _background = new Random(); // autonomous cosmetic environment stream
/// <summary>Reseed the simulation RNG. Call once when a run starts. The cosmetic stream is
/// seeded from a derived value so an input-replay still reproduces identical visuals, while the
/// streams stay independent (the derivations just decorrelate them).</summary>
public static void Seed( int seed )
{
_random = new Random( seed );
_cosmetic = new Random( unchecked( seed * 6151 + 1 ) );
_background = new Random( unchecked( seed * 7919 + 17 ) );
}
/// <summary>Integer in [min, max) — upper bound exclusive.</summary>
public static int Int( int min, int max )
{
if ( max <= min ) return min;
return _random.Next( min, max );
}
/// <summary>Float in [min, max).</summary>
public static float Float( float min, float max )
{
return min + (float)_random.NextDouble() * (max - min);
}
/// <summary>Float in [0, 1).</summary>
public static float Value()
{
return (float)_random.NextDouble();
}
// --- cosmetic stream: player-triggered visual effects only; NEVER affects gameplay state ------
/// <summary>Cosmetic integer in [min, max) — upper bound exclusive. Player-triggered visuals only.</summary>
public static int CosmeticInt( int min, int max )
{
if ( max <= min ) return min;
return _cosmetic.Next( min, max );
}
/// <summary>Cosmetic float in [min, max). Player-triggered visuals only.</summary>
public static float CosmeticFloat( float min, float max )
{
return min + (float)_cosmetic.NextDouble() * (max - min);
}
/// <summary>Cosmetic float in [0, 1). Player-triggered visuals only.</summary>
public static float CosmeticValue()
{
return (float)_cosmetic.NextDouble();
}
// --- background stream: autonomous cosmetic environment; never affects gameplay or particles ---
public static int BackgroundInt( int min, int max )
{
if ( max <= min ) return min;
return _background.Next( min, max );
}
public static float BackgroundFloat( float min, float max )
{
return min + (float)_background.NextDouble() * (max - min);
}
public static float BackgroundValue()
{
return (float)_background.NextDouble();
}
}