A small pseudorandom number generator utility. Implements xmur3 to hash a string to a 32-bit seed and mulberry32-style stream to produce floats, ints, weighted picks and simple probability checks for procedural music composition.
using System;
namespace Skafinity;
/// <summary>
/// The engine's PRNG — xmur3 hashes a seed string to 32 bits, mulberry32 streams from it.
/// Every musical choice comes out of here, which is why one seed gives one song.
///
/// Deliberately hand-rolled rather than <c>System.Random</c>: the algorithm is pinned by this
/// file, so it does not move under us when a runtime changes its implementation, and the same
/// source gives the same stream in the s&box library and the wasm bundle alike.
///
/// Streams are cheap and are meant to be forked liberally — the composer gives each voice in
/// each section its own <c>Rng</c> keyed on the section, so adding a draw to one voice cannot
/// shift what any other voice plays.
/// </summary>
sealed class Rng
{
uint _a;
public Rng( uint seed ) { _a = seed; }
/// <summary>A stream keyed on a string — the composer's usual entry point
/// (<c>"{tag}:bass:{section}"</c> and friends).</summary>
public Rng( string seed ) : this( Xmur3( seed ) ) { }
/// <summary>xmur3: string → a well-mixed 32-bit seed.</summary>
public static uint Xmur3( string str )
{
uint h = 1779033703u ^ (uint)str.Length;
for ( int i = 0; i < str.Length; i++ )
{
h = unchecked( (h ^ str[i]) * 3432918353u );
h = (h << 13) | (h >> 19);
}
h = unchecked( (h ^ (h >> 16)) * 2246822507u );
h = unchecked( (h ^ (h >> 13)) * 3266489909u );
return h ^ (h >> 16);
}
/// <summary>mulberry32: the next value in [0, 1).</summary>
public float Next()
{
_a = unchecked( _a + 0x6D2B79F5u );
uint t = _a;
t = unchecked( (t ^ (t >> 15)) * (t | 1u) );
t ^= unchecked( t + (t ^ (t >> 7)) * (t | 61u) );
return (t ^ (t >> 14)) / 4294967296f;
}
/// <summary>A value in [0, n). Clamped, so it is always a safe table index.</summary>
public int Int( int n ) => n <= 0 ? 0 : Math.Min( n - 1, (int)(Next() * n) );
public bool Chance( float p ) => Next() < p;
public T Pick<T>( T[] arr ) => arr[Int( arr.Length )];
/// <summary>A weighted draw — ONE <see cref="Next"/> whatever the weights are.
///
/// The tables used to bias a draw by listing an entry twice, which quietly tied "how likely"
/// to "how many entries" (and to the no-two-genres-share-more-than-one cap the engine test
/// enforces). A real weighted draw separates them, and costs the same single value out of
/// the song stream, so a genre's draw count still cannot depend on its tables.</summary>
/// <summary>The INDEX of a weighted draw — one <see cref="Next"/>, like
/// <see cref="PickWeighted"/>, for callers whose table is a parallel array rather than the
/// thing being picked (the melody's note lengths against a genre's weights over them).</summary>
public int WeightedIndex( int[] weights )
{
if ( weights == null || weights.Length == 0 ) return 0;
int total = 0;
foreach ( var w in weights ) total += Math.Max( 0, w );
if ( total <= 0 ) return Int( weights.Length );
float r = Next() * total;
for ( int i = 0; i < weights.Length; i++ )
{
r -= Math.Max( 0, weights[i] );
if ( r < 0f ) return i;
}
return weights.Length - 1;
}
public T PickWeighted<T>( T[] arr, int[] weights )
{
if ( arr == null || arr.Length == 0 ) return default;
if ( weights == null || weights.Length != arr.Length ) return Pick( arr );
int total = 0;
foreach ( var w in weights ) total += Math.Max( 0, w );
if ( total <= 0 ) return Pick( arr );
float r = Next() * total;
for ( int i = 0; i < arr.Length; i++ )
{
r -= Math.Max( 0, weights[i] );
if ( r < 0f ) return arr[i];
}
return arr[arr.Length - 1];
}
}