C
C#2y ago
Spectra

❔ ✅ Dice Roll with Generic Math

Has someone use non-generic funcs with generic math... The solution that I have found is a bit hack. There would be solutions that dont involves a bunch of conditionals?
public readonly struct Dice
{
public readonly int sides;

public Dice(int sides) => this.sides = sides;

public override string ToString() => sides.ToString();

public double GetAverage() => (sides + 1) / 2.0;

public int RollInt(Random rng) => rng.Next(1, sides + 1);
public long RollLong(Random rng) => rng.NextInt64() * sides + 1;
public float RollFloat(Random rng) => rng.NextSingle() * sides + 1;
public double RollDouble(Random rng) => rng.NextDouble() * sides + 1;

public T Roll<T>(Random rng) where T : INumber<T>
{
var type = typeof(T);
if (type == typeof(int))
return (T)(object)RollInt(rng);
if (type == typeof(long))
return (T)(object)RollLong(rng);
if (type == typeof(float))
return (T)(object)RollFloat(rng);
if (type == typeof(double))
return (T)(object)RollDouble(rng);
return T.One;
}
}
public readonly struct Dice
{
public readonly int sides;

public Dice(int sides) => this.sides = sides;

public override string ToString() => sides.ToString();

public double GetAverage() => (sides + 1) / 2.0;

public int RollInt(Random rng) => rng.Next(1, sides + 1);
public long RollLong(Random rng) => rng.NextInt64() * sides + 1;
public float RollFloat(Random rng) => rng.NextSingle() * sides + 1;
public double RollDouble(Random rng) => rng.NextDouble() * sides + 1;

public T Roll<T>(Random rng) where T : INumber<T>
{
var type = typeof(T);
if (type == typeof(int))
return (T)(object)RollInt(rng);
if (type == typeof(long))
return (T)(object)RollLong(rng);
if (type == typeof(float))
return (T)(object)RollFloat(rng);
if (type == typeof(double))
return (T)(object)RollDouble(rng);
return T.One;
}
}
30 Replies
Angius
Angius2y ago
Unless Random implements a generic method that uses INumber, not really I guess maybe a switch expression would shorten that code But that's about it
Aaron
Aaron2y ago
why not just
public T Roll<T>(Random rng) where T : INumber<T>
{
return T.CreateChecked(rng.Next(1, sides + 1));
}
public T Roll<T>(Random rng) where T : INumber<T>
{
return T.CreateChecked(rng.Next(1, sides + 1));
}
MODiX
MODiX2y ago
Windows10CE#8553
REPL Result: Success
int sides = 6;
public T Roll<T>(Random rng) where T : INumber<T>
{
return T.CreateChecked(rng.Next(1, sides + 1));
}
Roll<double>(Random.Shared)
int sides = 6;
public T Roll<T>(Random rng) where T : INumber<T>
{
return T.CreateChecked(rng.Next(1, sides + 1));
}
Roll<double>(Random.Shared)
Result: double
2
2
Compile: 560.954ms | Execution: 40.180ms | React with ❌ to remove this embed.
Spectra
SpectraOP2y ago
Well it's not exactly the same thing, but it's valid solution, if you don't care about lost in conversion. Well you can also use the double one so there would be no data loss, but less efficient. Well it's some kind of solution, good to know thx.
Aaron
Aaron2y ago
oh yeah, I didn't realize you had the the in-betweens as valid states yeah just swap Next for what you do in RollDouble
Spectra
SpectraOP2y ago
probaly is more efficient doing double rather than a bunch of ifs
Aaron
Aaron2y ago
CreateChecked is the same as checked((T) input)
DaVinki
DaVinki2y ago
If you would rather not lose anything in a conversion and support integers above 64-bit in size, you can: Stackalloc a byte span Use the random object to fill it with NexyBytes Use Unsafe.As
Aaron
Aaron2y ago
that isnt valid
DaVinki
DaVinki2y ago
What do you mean? If the type is unmanaged, it is So it would just add support for 128-bit but with a little more tampering you could absolutely do it for arbitrarily large types
Aaron
Aaron2y ago
it would break floats
MODiX
MODiX2y ago
Windows10CE#8553
REPL Result: Failure
uint u = uint.MaxValue;
Unsafe.As<uint, float>(ref u)
uint u = uint.MaxValue;
Unsafe.As<uint, float>(ref u)
Exception: ArgumentException
- An exception occurred when serializing the response: ArgumentException: .NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling).
- An exception occurred when serializing the response: ArgumentException: .NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling).
Compile: 475.020ms | Execution: 31.323ms | React with ❌ to remove this embed.
DaVinki
DaVinki2y ago
Why did you test that with uint
Aaron
Aaron2y ago
easiest way to get four bytes of 0xFF its the same as doing
MODiX
MODiX2y ago
Windows10CE#8553
REPL Result: Failure
unsafe
{
Span<byte> bytes = stackalloc byte[4];
bytes.Fill(0xFF);
Console.WriteLine(Unsafe.As(ref bytes[0]));
}
unsafe
{
Span<byte> bytes = stackalloc byte[4];
bytes.Fill(0xFF);
Console.WriteLine(Unsafe.As(ref bytes[0]));
}
Exception: CompilationErrorException
- Parameters or locals of type 'Span<byte>' cannot be declared in async methods or async lambda expressions.
- The type arguments for method 'Unsafe.As<T>(object?)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
- Parameters or locals of type 'Span<byte>' cannot be declared in async methods or async lambda expressions.
- The type arguments for method 'Unsafe.As<T>(object?)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
Compile: 603.036ms | Execution: 0.000ms | React with ❌ to remove this embed.
Aaron
Aaron2y ago
but easier also oof
MODiX
MODiX2y ago
Windows10CE#8553
REPL Result: Success
static unsafe void Main()
{
Span<byte> bytes = stackalloc byte[4];
bytes.Fill(0xFF);
Console.WriteLine(Unsafe.As<byte, float>(ref bytes[0]));
}
Main();
static unsafe void Main()
{
Span<byte> bytes = stackalloc byte[4];
bytes.Fill(0xFF);
Console.WriteLine(Unsafe.As<byte, float>(ref bytes[0]));
}
Main();
Console Output
NaN
NaN
Compile: 607.462ms | Execution: 37.521ms | React with ❌ to remove this embed.
Aaron
Aaron2y ago
i really shouldnt write unsafe code on discord but yeah, converting arbitrary bytes to a float is a great way to get random NaNs and Infs (but yes, if floats weren't involved, and you didn't care about bounding the output, this works fine)
Anton
Anton2y ago
those ifs don't cost you anything, they resolve as constants at jit time and the dead branches get eliminated. the function then will likely get inlined. I'm only not sure about (object) conversions, that's a thing I have wondered previously, but the boxing should also get eliminated at jit time but check out the perf yourself the above is what I've heard from other people
Aaron
Aaron2y ago
all that is correct as well, if you're fine with what you have now i find return T.CreateChecked(rng.NextDouble() * sides + 1); to be much easier to read though though if you want the range to be [1, sides] instead of [1, sides + 1) you have to do
return T.CreateChecked(double.Min(rng.NextDouble() * sides + 1, sides));
return T.CreateChecked(double.Min(rng.NextDouble() * sides + 1, sides));
Anton
Anton2y ago
I think you've mentioned that, but double has less precision than e.g. long for integers, so it's not the same implicit conversion of the compiler from long to double or from int to float is a lie made for convenience
Aaron
Aaron2y ago
as long as sides isn't very large it doesn't matter much actually i think this weights to die on sides, kekw actually
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
hm no that doesnt work either because for integers it will be very unlikely to get sides honestly
if (default(T) is IFloatingPoint<T>)
{
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
}
else
{
return T.CreateChecked(rng.NextInt64(1, sides + 1));
}
if (default(T) is IFloatingPoint<T>)
{
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
}
else
{
return T.CreateChecked(rng.NextInt64(1, sides + 1));
}
i think that works the best? for both integers and floats that will get you the range [1, sides] with equal distribution (assuming System.Random has equal distribution, of course)
Anton
Anton2y ago
your double can output 0
Aaron
Aaron2y ago
no it can't +1 on the end
Anton
Anton2y ago
ah yeah you're right
Aaron
Aaron2y ago
aaaaaaa that is check doesnt compile i'm back with a vengence
if (T.One / (T.One + T.One) == T.Zero)
{
return T.CreateChecked(rng.NextInt64(1, sides + 1));
}
else
{
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
}
if (T.One / (T.One + T.One) == T.Zero)
{
return T.CreateChecked(rng.NextInt64(1, sides + 1));
}
else
{
return T.CreateChecked(rng.NextDouble() * (sides - 1) + 1);
}
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Spectra
SpectraOP2y ago
Have done some benchmarks, there almost no difference it completely depends on the types that is been casted. And there is a problem with (T)(object) which the cast can throw exception while the others don't.
Spectra
SpectraOP2y ago
I think this can be closed so...
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server