diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 493959482cf..feb9992ece6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,4 +1,4 @@ -# Release notes for RobustToolbox. +# Release notes for RobustToolbox. ### New features -*None yet* +* Added `IRobustRandom.GetItems` extension methods for randomly picking multiple items from a collections. ### Bugfixes @@ -51,7 +51,7 @@ END TEMPLATE--> ### Internal -*None yet* +* `Shuffle(Span, System.Random)` has been removed, just use the builtin method. ## 217.0.0 diff --git a/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs b/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs index 003a4fb399d..f05c0f48e90 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs @@ -36,7 +36,7 @@ public void LoadDirectory(ResPath path, bool overwrite = false, .ToArray(); // Shuffle to avoid input data patterns causing uneven thread workloads. - RandomExtensions.Shuffle(streams.AsSpan(), new System.Random()); + (new System.Random()).Shuffle(streams.AsSpan()); var sawmill = _logManager.GetSawmill("eng"); diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 3149e5213dd..4292ef58292 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -432,7 +432,7 @@ public void ResolveResults() }).ToArray(); // Randomize to remove any patterns that could cause uneven load. - RandomExtensions.Shuffle(allResults.AsSpan(), rand); + rand.Shuffle(allResults.AsSpan()); // Create channel that all AfterDeserialization hooks in this group will be sent into. var hooksChannelOptions = new UnboundedChannelOptions diff --git a/Robust.Shared/Random/IRobustRandom.cs b/Robust.Shared/Random/IRobustRandom.cs index b7a0e2bdbdd..4e368c74822 100644 --- a/Robust.Shared/Random/IRobustRandom.cs +++ b/Robust.Shared/Random/IRobustRandom.cs @@ -1,56 +1,141 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Collections; using Robust.Shared.Maths; -using Robust.Shared.Toolshed.Commands.Generic; namespace Robust.Shared.Random; +/// +/// Wrapper around random number generator helping methods. +/// public interface IRobustRandom { - /// - /// Get the underlying System.Random - /// - /// + /// Get the underlying . System.Random GetRandom(); + + /// Set seed for underlying . void SetSeed(int seed); + /// Get random value between 0 (included) and 1 (excluded). float NextFloat(); + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. public float NextFloat(float minValue, float maxValue) => NextFloat() * (maxValue - minValue) + minValue; + + /// Get random value in range of 0 (included) and (excluded). + /// Random value should be less then this value. public float NextFloat(float maxValue) => NextFloat() * maxValue; + + /// Get random value. int Next(); - int Next(int minValue, int maxValue); - TimeSpan Next(TimeSpan minTime, TimeSpan maxTime); - TimeSpan Next(TimeSpan maxTime); + + /// Get random value in range of 0 (included) and (excluded). + /// Random value should be less then this value. int Next(int maxValue); + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. + int Next(int minValue, int maxValue); + + /// Get random value between 0 (included) and (excluded). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte NextByte() + => NextByte(byte.MaxValue); + + /// Get random value in range of 0 (included) and (excluded). + /// Random value should be less then this value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte NextByte(byte maxValue) + => NextByte(0, maxValue); + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte NextByte(byte minValue, byte maxValue) + => (byte)Next(minValue, maxValue); + + /// Get random value between 0 (included) and 1 (excluded). double NextDouble(); - double NextDouble(double minValue, double maxValue) => NextDouble() * (maxValue - minValue) + minValue; + + /// Get random value in range of 0 (included) and (excluded). + /// Random value should be less then this value. + double Next(double maxValue) + => NextDouble() * maxValue; + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. + double NextDouble(double minValue, double maxValue) + => NextDouble() * (maxValue - minValue) + minValue; + + /// Get random value in range of (included) and (excluded). + /// Random value should be less then this value. + TimeSpan Next(TimeSpan maxTime); + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. + TimeSpan Next(TimeSpan minTime, TimeSpan maxTime); + + /// Fill buffer with random bytes (values). void NextBytes(byte[] buffer); - public Angle NextAngle() => NextFloat() * MathF.Tau; - public Angle NextAngle(Angle minValue, Angle maxValue) => NextFloat() * (maxValue - minValue) + minValue; - public Angle NextAngle(Angle maxValue) => NextFloat() * maxValue; + /// Get random value in range of 0 (included) and (excluded). + public Angle NextAngle() + => NextFloat() * MathF.Tau; + + /// Get random value in range of 0 (included) and (excluded). + /// Random value should be less then this value. + public Angle NextAngle(Angle maxValue) + => NextFloat() * maxValue; + + /// Get random value in range of (included) and (excluded). + /// Random value should be greater or equal to this value. + /// Random value should be less then this value. + public Angle NextAngle(Angle minValue, Angle maxValue) + => NextFloat() * (maxValue - minValue) + minValue; + + /// + /// Random vector, created from a uniform distribution of magnitudes and angles. + /// + /// Max value for randomized vector magnitude (excluded). + public Vector2 NextVector2(float maxMagnitude = 1) + => NextVector2(0, maxMagnitude); /// /// Random vector, created from a uniform distribution of magnitudes and angles. /// + /// Min value for randomized vector magnitude (included). + /// Max value for randomized vector magnitude (excluded). /// /// In general, NextVector2(1) will tend to result in vectors with smaller magnitudes than /// NextVector2Box(1,1), even if you ignored any vectors with a magnitude larger than one. /// - public Vector2 NextVector2(float minMagnitude, float maxMagnitude) => NextAngle().RotateVec(new Vector2(NextFloat(minMagnitude, maxMagnitude), 0)); - public Vector2 NextVector2(float maxMagnitude = 1) => NextVector2(0, maxMagnitude); + public Vector2 NextVector2(float minMagnitude, float maxMagnitude) + => NextAngle().RotateVec(new Vector2(NextFloat(minMagnitude, maxMagnitude), 0)); /// /// Random vector, created from a uniform distribution of x and y coordinates lying inside some box. /// - public Vector2 NextVector2Box(float minX, float minY, float maxX, float maxY) => new Vector2(NextFloat(minX, maxX), NextFloat(minY, maxY)); - public Vector2 NextVector2Box(float maxAbsX = 1, float maxAbsY = 1) => NextVector2Box(-maxAbsX, -maxAbsY, maxAbsX, maxAbsY); + public Vector2 NextVector2Box(float minX, float minY, float maxX, float maxY) + => new Vector2(NextFloat(minX, maxX), NextFloat(minY, maxY)); + /// + /// Random vector, created from a uniform distribution of x and y coordinates lying inside some box. + /// Box will have coordinates starting at [- , -] + /// and ending in [ , ] + /// + public Vector2 NextVector2Box(float maxAbsX = 1, float maxAbsY = 1) + => NextVector2Box(-maxAbsX, -maxAbsY, maxAbsX, maxAbsY); + + /// Randomly switches positions in collection. void Shuffle(IList list) { var n = list.Count; @@ -62,6 +147,7 @@ void Shuffle(IList list) } } + /// Randomly switches positions in collection. void Shuffle(Span list) { var n = list.Length; @@ -73,6 +159,7 @@ void Shuffle(Span list) } } + /// Randomly switches positions in collection. void Shuffle(ValueList list) { var n = list.Count; @@ -83,24 +170,6 @@ void Shuffle(ValueList list) (list[k], list[n]) = (list[n], list[k]); } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte NextByte(byte maxValue) - { - return NextByte(0, maxValue); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte NextByte() - { - return NextByte(byte.MaxValue); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte NextByte(byte minValue, byte maxValue) - { - return (byte) Next(minValue, maxValue); - } } public static class RandomHelpers @@ -136,6 +205,6 @@ public static byte NextByte(this System.Random random) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte NextByte(this System.Random random, byte minValue, byte maxValue) { - return (byte) random.Next(minValue, maxValue); + return (byte)random.Next(minValue, maxValue); } } diff --git a/Robust.Shared/Random/RandomExtensions.cs b/Robust.Shared/Random/RandomExtensions.cs index a78725470fb..079e1981586 100644 --- a/Robust.Shared/Random/RandomExtensions.cs +++ b/Robust.Shared/Random/RandomExtensions.cs @@ -1,160 +1,263 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Robust.Shared.Collections; using Robust.Shared.Maths; using Robust.Shared.Utility; -namespace Robust.Shared.Random +namespace Robust.Shared.Random; + +public static class RandomExtensions { - public static class RandomExtensions - { - /// - /// Generate a random number from a normal (gaussian) distribution. - /// - /// The random object to generate the number from. - /// The average or "center" of the normal distribution. - /// The standard deviation of the normal distribution. - public static double NextGaussian(this IRobustRandom random, double μ = 0, double σ = 1) - { - return random.GetRandom().NextGaussian(μ, σ); - } + /// + /// Generate a random number from a normal (gaussian) distribution. + /// + /// The random object to generate the number from. + /// The average or "center" of the normal distribution. + /// The standard deviation of the normal distribution. + public static double NextGaussian(this IRobustRandom random, double μ = 0, double σ = 1) + { + return random.GetRandom().NextGaussian(μ, σ); + } - public static T Pick(this IRobustRandom random, IReadOnlyList list) - { - var index = random.Next(list.Count); - return list[index]; - } + /// Picks a random element from a collection. + public static T Pick(this IRobustRandom random, IReadOnlyList list) + { + var index = random.Next(list.Count); + return list[index]; + } - public static ref T Pick(this IRobustRandom random, ValueList list) - { - var index = random.Next(list.Count); - return ref list[index]; - } + /// Picks a random element from a collection. + public static ref T Pick(this IRobustRandom random, ValueList list) + { + var index = random.Next(list.Count); + return ref list[index]; + } - public static ref T Pick(this System.Random random, ValueList list) + /// Picks a random element from a collection. + public static ref T Pick(this System.Random random, ValueList list) + { + var index = random.Next(list.Count); + return ref list[index]; + } + + /// Picks a random element from a collection. + /// + /// This is O(n). + /// + public static T Pick(this IRobustRandom random, IReadOnlyCollection collection) + { + var index = random.Next(collection.Count); + var i = 0; + foreach (var t in collection) { - var index = random.Next(list.Count); - return ref list[index]; + if (i++ == index) + { + return t; + } } - /// Picks a random element from a collection. - /// - /// This is O(n). - /// - public static T Pick(this IRobustRandom random, IReadOnlyCollection collection) + throw new UnreachableException("This should be unreachable!"); + } + + /// + /// Picks a random element from a list, removes it from list and returns it. + /// This is O(n) as it preserves the order of other items in the list. + /// + public static T PickAndTake(this IRobustRandom random, IList list) + { + var index = random.Next(list.Count); + var element = list[index]; + list.RemoveAt(index); + return element; + } + + /// + /// Picks a random element from a set and returns it. + /// This is O(n) as it has to iterate the collection until the target index. + /// + public static T Pick(this System.Random random, ICollection collection) + { + var index = random.Next(collection.Count); + var i = 0; + foreach (var t in collection) { - var index = random.Next(collection.Count); - var i = 0; - foreach (var t in collection) + if (i++ == index) { - if (i++ == index) - { - return t; - } + return t; } - - throw new UnreachableException("This should be unreachable!"); } - public static T PickAndTake(this IRobustRandom random, IList list) + throw new UnreachableException("This should be unreachable!"); + } + + /// + /// Picks a random from a collection then removes it and returns it. + /// This is O(n) as it has to iterate the collection until the target index. + /// + public static T PickAndTake(this System.Random random, ICollection set) + { + var tile = Pick(random, set); + set.Remove(tile); + return tile; + } + + /// + /// Generate a random number from a normal (gaussian) distribution. + /// + /// The random object to generate the number from. + /// The average or "center" of the normal distribution. + /// The standard deviation of the normal distribution. + public static double NextGaussian(this System.Random random, double μ = 0, double σ = 1) + { + // https://stackoverflow.com/a/218600 + var α = random.NextDouble(); + var β = random.NextDouble(); + + var randStdNormal = Math.Sqrt(-2.0 * Math.Log(α)) * Math.Sin(2.0 * Math.PI * β); + + return μ + σ * randStdNormal; + } + + public static Angle NextAngle(this System.Random random) => NextFloat(random) * MathF.Tau; + + public static Angle NextAngle(this System.Random random, Angle minAngle, Angle maxAngle) + { + DebugTools.Assert(minAngle < maxAngle); + return minAngle + (maxAngle - minAngle) * random.NextDouble(); + } + + public static float NextFloat(this IRobustRandom random) + { + // This is pretty much the CoreFX implementation. + // So credits to that. + // Except using float instead of double. + return random.Next() * 4.6566128752458E-10f; + } + + public static float NextFloat(this System.Random random) + { + return random.Next() * 4.6566128752458E-10f; + } + + /// + /// Have a certain chance to return a boolean. + /// + /// The random instance to run on. + /// The chance to pass, from 0 to 1. + public static bool Prob(this IRobustRandom random, float chance) + { + DebugTools.Assert(chance <= 1 && chance >= 0, $"Chance must be in the range 0-1. It was {chance}."); + + return random.NextDouble() < chance; + } + + /// + /// Get set amount of random items from a collection. + /// If is false and + /// is smaller then - returns shuffled clone. + /// If is empty, and/or is 0, returns empty. + /// + /// Instance of random to invoke upon. + /// Collection from which items should be picked. + /// Number of random items to be picked. + /// If true, items are allowed to be picked more than once. + public static T[] GetItems(this IRobustRandom random, IList source, int count, bool allowDuplicates = true) + { + if (source.Count == 0 || count <= 0) + return Array.Empty(); + + if (allowDuplicates == false && count >= source.Count) { - var index = random.Next(list.Count); - var element = list[index]; - list.RemoveAt(index); - return element; + var arr = source.ToArray(); + random.Shuffle(arr); + return arr; } - /// - /// Picks a random element from a set and returns it. - /// This is O(n) as it has to iterate the collection until the target index. - /// - public static T Pick(this System.Random random, ICollection collection) + var sourceCount = source.Count; + var result = new T[count]; + + if (allowDuplicates) { - var index = random.Next(collection.Count); - var i = 0; - foreach (var t in collection) + for (var i = 0; i < count; i++) { - if (i++ == index) - { - return t; - } + result[i] = source[random.Next(sourceCount)]; } - throw new UnreachableException("This should be unreachable!"); + return result; } - /// - /// Picks a random from a collection then removes it and returns it. - /// This is O(n) as it has to iterate the collection until the target index. - /// - public static T PickAndTake(this System.Random random, ICollection set) + var indices = sourceCount <= 1024 ? stackalloc int[sourceCount] : new int[sourceCount]; + for (var i = 0; i < sourceCount; i++) { - var tile = Pick(random, set); - set.Remove(tile); - return tile; + indices[i] = i; } - /// - /// Generate a random number from a normal (gaussian) distribution. - /// - /// The random object to generate the number from. - /// The average or "center" of the normal distribution. - /// The standard deviation of the normal distribution. - public static double NextGaussian(this System.Random random, double μ = 0, double σ = 1) + for (var i = 0; i < count; i++) { - // https://stackoverflow.com/a/218600 - var α = random.NextDouble(); - var β = random.NextDouble(); + var j = random.Next(sourceCount - i); + result[i] = source[indices[j]]; + indices[j] = indices[sourceCount - i - 1]; + } - var randStdNormal = Math.Sqrt(-2.0 * Math.Log(α)) * Math.Sin(2.0 * Math.PI * β); + return result; + } - return μ + σ * randStdNormal; - } + /// + public static T[] GetItems(this IRobustRandom random, ValueList source, int count, bool allowDuplicates = true) + { + return GetItems(random, source.Span, count, allowDuplicates); + } - public static Angle NextAngle(this System.Random random) => NextFloat(random) * MathF.Tau; + /// + public static T[] GetItems(this IRobustRandom random, T[] source, int count, bool allowDuplicates = true) + { + return GetItems(random, source.AsSpan(), count, allowDuplicates); + } - public static Angle NextAngle(this System.Random random, Angle minAngle, Angle maxAngle) - { - DebugTools.Assert(minAngle < maxAngle); - return minAngle + (maxAngle - minAngle) * random.NextDouble(); - } + /// + public static T[] GetItems(this IRobustRandom random, Span source, int count, bool allowDuplicates = true) + { + if (source.Length == 0 || count <= 0) + return Array.Empty(); - public static float NextFloat(this IRobustRandom random) + if (allowDuplicates == false && count >= source.Length) { - // This is pretty much the CoreFX implementation. - // So credits to that. - // Except using float instead of double. - return random.Next() * 4.6566128752458E-10f; + var arr = source.ToArray(); + random.Shuffle(arr); + return arr; } - public static float NextFloat(this System.Random random) + var sourceCount = source.Length; + var result = new T[count]; + + if (allowDuplicates) { - return random.Next() * 4.6566128752458E-10f; + // TODO RANDOM consider just using System.Random.GetItems() + // However, the different implementations might mean that lists & arrays shuffled using the same seed + // generate different results, which might be undesirable? + for (var i = 0; i < count; i++) + { + result[i] = source[random.Next(sourceCount)]; + } + + return result; } - /// - /// Have a certain chance to return a boolean. - /// - /// The random instance to run on. - /// The chance to pass, from 0 to 1. - public static bool Prob(this IRobustRandom random, float chance) + var indices = sourceCount <= 1024 ? stackalloc int[sourceCount] : new int[sourceCount]; + for (var i = 0; i < sourceCount; i++) { - DebugTools.Assert(chance <= 1 && chance >= 0, $"Chance must be in the range 0-1. It was {chance}."); - - return random.NextDouble() < chance; + indices[i] = i; } - internal static void Shuffle(Span array, System.Random random) + for (var i = 0; i < count; i++) { - var n = array.Length; - while (n > 1) - { - n--; - var k = random.Next(n + 1); - (array[k], array[n]) = - (array[n], array[k]); - } + var j = random.Next(sourceCount - i); + result[i] = source[indices[j]]; + indices[j] = indices[sourceCount - i - 1]; } + + return result; } } diff --git a/Robust.Shared/Random/RobustRandom.cs b/Robust.Shared/Random/RobustRandom.cs index 28981c76fb6..4f447b2e64c 100644 --- a/Robust.Shared/Random/RobustRandom.cs +++ b/Robust.Shared/Random/RobustRandom.cs @@ -1,58 +1,65 @@ using System; using Robust.Shared.Utility; -namespace Robust.Shared.Random +namespace Robust.Shared.Random; + +/// +/// Wrapper for . +/// +/// +/// This should not contain any logic, not directly related to calling specific methods of . +/// To write additional logic, attached to random roll, please create interface-implemented methods on +/// or add it to . +/// +public sealed class RobustRandom : IRobustRandom { - public sealed class RobustRandom : IRobustRandom - { - private System.Random _random = new(); - - public System.Random GetRandom() => _random; - - public void SetSeed(int seed) - { - _random = new(seed); - } - - public float NextFloat() - { - return _random.NextFloat(); - } - - public int Next() - { - return _random.Next(); - } - - public int Next(int minValue, int maxValue) - { - return _random.Next(minValue, maxValue); - } - - public TimeSpan Next(TimeSpan minTime, TimeSpan maxTime) - { - DebugTools.Assert(minTime < maxTime); - return minTime + (maxTime - minTime) * _random.NextDouble(); - } - - public TimeSpan Next(TimeSpan maxTime) - { - return Next(TimeSpan.Zero, maxTime); - } - - public int Next(int maxValue) - { - return _random.Next(maxValue); - } - - public double NextDouble() - { - return _random.NextDouble(); - } - - public void NextBytes(byte[] buffer) - { - _random.NextBytes(buffer); - } + private System.Random _random = new(); + + public System.Random GetRandom() => _random; + + public void SetSeed(int seed) + { + _random = new(seed); + } + + public float NextFloat() + { + return _random.NextFloat(); + } + + public int Next() + { + return _random.Next(); + } + + public int Next(int minValue, int maxValue) + { + return _random.Next(minValue, maxValue); + } + + public TimeSpan Next(TimeSpan minTime, TimeSpan maxTime) + { + DebugTools.Assert(minTime < maxTime); + return minTime + (maxTime - minTime) * _random.NextDouble(); + } + + public TimeSpan Next(TimeSpan maxTime) + { + return Next(TimeSpan.Zero, maxTime); + } + + public int Next(int maxValue) + { + return _random.Next(maxValue); + } + + public double NextDouble() + { + return _random.NextDouble(); + } + + public void NextBytes(byte[] buffer) + { + _random.NextBytes(buffer); } } diff --git a/Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs b/Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs new file mode 100644 index 00000000000..15c66fa0825 --- /dev/null +++ b/Robust.UnitTesting/Shared/Random/RandomExtensionsTests.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using NUnit.Framework; +using Robust.Shared.Collections; +using Robust.Shared.Random; + +// ReSharper disable AccessToStaticMemberViaDerivedType + +namespace Robust.UnitTesting.Shared.Random; + +/// Instantiable tests for . +[TestFixture] +public sealed class RandomExtensionsGetItemsWithListTests : RandomExtensionsTests> +{ + /// + protected override IList CreateCollection() + => new List(CollectionForTests); + + /// + protected override IReadOnlyCollection Invoke(IList collection, int count, bool allowDuplicates) + => _underlyingRandom.GetItems(collection, count, allowDuplicates); +} + +/// Instantiable tests for . +[TestFixture] +public sealed class RandomExtensionsGetItemsWithSpanTests : RandomExtensionsTests +{ + /// + protected override string[] CreateCollection() + => CollectionForTests; + + /// + protected override IReadOnlyCollection Invoke(string[] collection, int count, bool allowDuplicates) + { + var span = new Span(collection); + return _underlyingRandom.GetItems(span, count, allowDuplicates) + .ToArray(); + } +} + +/// Instantiable tests for . +[TestFixture] +public sealed class RandomExtensionsGetItemsWithValueListTests : RandomExtensionsTests> +{ + /// + protected override ValueList CreateCollection() + => new ValueList(CollectionForTests); + + /// + protected override IReadOnlyCollection Invoke(ValueList collection, int count, bool allowDuplicates) + => _underlyingRandom.GetItems(collection, count, allowDuplicates) + .ToArray(); +} + +[TestFixture] +public abstract class RandomExtensionsTests +{ + protected IRobustRandom _underlyingRandom = default!; + + protected readonly string[] CollectionForTests = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" }; + + private T _collection = default!; + + private int Count => CollectionForTests.Length; + + [SetUp] + public void Setup() + { + _underlyingRandom = Mock.Of(); + _collection = CreateCollection(); + } + + [Test] + public void GetItems_PickOneFromList_ReturnOfRandomizedIndex() + { + // Arrange + Mock.Get(_underlyingRandom) + .Setup(x => x.Next(Count)) + .Returns(8); + + // Act + var result = Invoke(_collection, 1, true); + + // Assert + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.Single(), Is.EqualTo("8")); + } + + + [Test] + public void GetItems_PickOneFromListWithoutDuplicates_ReturnOfRandomizedIndex() + { + // Arrange + Mock.Get(_underlyingRandom) + .Setup(x => x.Next(Count)) + .Returns(8); + + // Act + var result = Invoke(_collection, 1, allowDuplicates: false); + + // Assert + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.Single(), Is.EqualTo("8")); + } + + [Test] + public void GetItems_PickSomeFromList_ReturnOfRandomizedIndex() + { + // Arrange + Mock.Get(_underlyingRandom) + .SetupSequence(x => x.Next(Count)) + .Returns(8) + .Returns(3) + .Returns(2); + + // Act + var result = Invoke(_collection, 3, true); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "8", "3", "2" })); + } + + [Test] + public void GetItems_PickSomeFromListWhileRollingDuplicates_ReturnWithDuplicates() + { + // Arrange + Mock.Get(_underlyingRandom) + .SetupSequence(x => x.Next(Count)) + .Returns(8) + .Returns(2) + .Returns(2) + .Returns(2); + + // Act + var result = Invoke(_collection, 4, allowDuplicates: true); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "8", "2", "2", "2" })); + } + + [Test] + public void GetItems_PickSameAmountAsOriginalCollection_ReturnWithDuplicates() + { + // Arrange + Mock.Get(_underlyingRandom) + .SetupSequence(x => x.Next(Count)) + .Returns(0) + .Returns(2) + .Returns(2) + .Returns(4) + .Returns(6) + .Returns(5) + .Returns(4) + .Returns(3) + .Returns(2) + .Returns(1) + .Returns(0); + + // Act + var result = Invoke(_collection, 11, allowDuplicates: true); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "0", "2", "2", "4", "6", "5", "4", "3", "2", "1", "0" })); + } + + [Test] + public void GetItems_PickMoreItemsThenOriginalCollectionHave_ReturnWithDuplicates() + { + // Arrange + Mock.Get(_underlyingRandom) + .SetupSequence(x => x.Next(Count)) + .Returns(0) + .Returns(2) + .Returns(2) + .Returns(4) + .Returns(6) + .Returns(5) + .Returns(4) + .Returns(3) + .Returns(2) + .Returns(1) + .Returns(9) + .Returns(9); + + // Act + var result = Invoke(_collection, 12, allowDuplicates: true); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "0", "2", "2", "4", "6", "5", "4", "3", "2", "1", "9", "9" })); + } + + [Test] + public void GetItems_PickSomeItemsWithoutDuplicates_ReturnWithoutDuplicates() + { + // Arrange + var mock = Mock.Get(_underlyingRandom); + mock.Setup(x => x.Next(Count)).Returns(1); + mock.Setup(x => x.Next(Count - 1)).Returns(1); + mock.Setup(x => x.Next(Count - 2)).Returns(6); + + // Act + var result = Invoke(_collection, 3, allowDuplicates: false); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "1", "10", "6" })); + } + + [Test] + public void GetItems_PickOneLessItemsThenOriginalCollectionHaveWithoutDuplicates_ReturnWithoutDuplicates() + { + // Arrange + var mock = Mock.Get(_underlyingRandom); + mock.Setup(x => x.Next(Count)).Returns(1); + mock.Setup(x => x.Next(Count - 1)).Returns(1); + mock.Setup(x => x.Next(Count - 2)).Returns(6); + mock.Setup(x => x.Next(Count - 3)).Returns(6); + mock.Setup(x => x.Next(Count - 4)).Returns(3); + mock.Setup(x => x.Next(Count - 5)).Returns(4); + mock.Setup(x => x.Next(Count - 6)).Returns(4); + mock.Setup(x => x.Next(Count - 7)).Returns(3); + mock.Setup(x => x.Next(Count - 8)).Returns(1); + mock.Setup(x => x.Next(Count - 9)).Returns(1); + + // Act + var result = Invoke(_collection, 10, allowDuplicates: false); + + // Assert + Assert.That(result, Is.EqualTo(new[] { "1", "10", "6", "8", "3", "4", "5", "7", "9", "2" })); + } + + [Test] + public void GetItems_PickAllItemsWithoutDuplicates_ReturnOriginalCollectionShuffledWithoutDuplicates() + { + // Arrange + var shuffled = new[] { "9", "0", "4", "2", "3", "7", "5", "8", "6", "10", "1" }; + Mock.Get(_underlyingRandom) + .Setup(x => x.Shuffle(It.IsAny>())) + .Callback>(x => + { + for (int i = 0; i < shuffled.Length; i++) + { + x[i] = shuffled[i]; + } + }); + + // Act + var result = Invoke(_collection, 11, allowDuplicates: false); + + // Assert + Assert.That(result, Is.EqualTo(shuffled)); + Mock.Get(_underlyingRandom).Verify(x => x.Next(It.IsAny()), Times.Never); + } + + [Test] + public void GetItems_PickMoreItemsThenOriginalHaveWithoutDuplicates_ReturnOriginalShuffledOriginalCollectionWithoutDuplicates() + { + // Arrange + var shuffled = new[] { "9", "0", "4", "2", "3", "7", "5", "8", "6", "10", "1" }; + Mock.Get(_underlyingRandom) + .Setup(x => x.Shuffle(It.IsAny>())) + .Callback>(x => + { + for (int i = 0; i < shuffled.Length; i++) + { + x[i] = shuffled[i]; + } + }); + + // Act + var result = Invoke(_collection, 30, allowDuplicates: false); + + // Assert + Assert.That(result, Is.EqualTo(shuffled)); + Mock.Get(_underlyingRandom).Verify(x => x.Next(It.IsAny()), Times.Never); + } + + /// Create concrete collection for tests. + protected abstract T CreateCollection(); + + /// Invoke method under test. Separate implementation types will have different overrides to be tested. + protected abstract IReadOnlyCollection Invoke(T collection, int count, bool allowDuplicates); +}