C# Игральный кубик с неравномерной вероятностью
Имеется игральный кубик с N количеством граней, сейчас вполне хватит 6. Кубик является не вполне честным, так как шанс выпадения граней неравномерный. Тем не менее, при броске гарантированно выпадает одна из граней.
Шансы выпадения граней этого кубика:
- 10%
- 15%
- 40%
- 5%
- 10%
- 20%
Примерная структура игрального кубика:
public struct Dice
{
/// <summary>
/// Edges of a <see cref="Dice"/>
/// </summary>
public Dictionary<int, double> Edges { get; private set; }
public Dice(Dictionary<int, double> edges)
{
this.Edges = edges;
}
public int Roll()
{
// some code
return default;
}
}
Как можно реализовать бросок такого кубика, а затем вернуть номер выпавшей грани?
Ответы (5 шт):
public int Roll()
{
Random random = new Random();
double r = random.NextDouble();
double p = 0;
foreach(int key in edges.Keys)
{
if (p <= r && r < p + edges[key])
return key;
p = p + edges[key];
}
return -1;
}
Как альтернатива, более шустрый и тупой вариант (но в общем случае неудобный и не везде применимый) это создать массив с нужным количеством тех или иных вариантов и выбирать уже из него рандомом по его размеру:
public int Roll()
{
Random random = new Random();
var dist = new []{1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 5, 6, 6, 6, 6};
return dist[random.Next(dist.Length)];
}
* edges здесь не учитывается
Для датчика с равномерным распределением случайных чисел на отрезке [A,B], вероятность попадания в некоторый отрезок [a,b], принадлежащий [A,B] (a>A и b<B) пропорциональна длине этого отрезка (b-a)/(B-A). Следовательно Ваша задача разбить интервал распределения случайной величины вашего датчика на множество не пересекающихся отрезков, с длиной пропорциональной заданной вероятности.
Для того создаем в Вашем классе генератор случайных чисел:
public class Dice
{
//...
Random rnd;
//...
}
Поскольку у Вас шансы выпадения граней заданы в процентах, то дабы не заморачиваться будем генерировать целые случайные числа в интервале 1,100. Тогда Ваша искомая функция будет иметь вид:
public int Roll()
{
int p = rnd.Next(1, 101); // правая граница Random не входит в интервал
if (p <= 10) return 1;
if (p > 10 && p <= 25) return 2;
if (p > 25 && p <= 65) return 3;
if (p > 65 && p <= 70) return 4;
if (p > 70 && p <= 80) return 5;
if (p > 80) return 6;
throw new Exception("Вот сейчас совсем неожиданно было");
}
То же самое, только с использованием синтаксиса C# 9.0 (.NET 5)
public int Roll() => rnd.Next(1, 101) switch
{
> 80 => 6,
> 70 => 5,
> 65 => 4,
> 25 => 3,
> 10 => 2,
_ => 1
};
Не успел до ответа @Александра Муксимова. По сути, мой ответ содержит те же идеи, что и у автора принятого ответа.
Реализация проста. Необходимо представить в голове колесо рулетки, где каждый сектор - это граница кубика, а его размер - это вероятность выпадения грани кубика. Т.е. процесс бросания кубика превращается в процесс вращения рулетки. Вот, как это можно представить:
Необходимо этот процесс вращения аппроксимировать на прямую длиной от 0 до 1. Т.е. необходимо разбить прямую на "секторы", где длина сектора вычисляется по формуле:
L(сектора i) = (1 / 100) * P(вероятность выпадения сектора i в процентах).
Реализация алгоритма представлена ниже. Не делал проверок на правильность указанных значений в edges, поскольку хотел показать основную идею, а дописать ее будет несложно.
private static void Main(string[] args)
{
var edges = new Dictionary<int, double>
{
{1, 10},
{2, 15},
{3, 40},
{4, 5},
{5, 10},
{6, 20}
};
var dice = new Dice(edges);
var rollCount = 100;
var actualProbabilities = edges.Select(p => p.Key).ToDictionary(p => p, p => 0);
for (var i = 0; i < rollCount; i++)
{
var edge = dice.Roll();
actualProbabilities[edge]++;
Console.WriteLine($"Roll #{i}: Edge {edge}");
}
foreach (var p in actualProbabilities)
{
Console.WriteLine($"Edge #{p.Key} has actual probability {p.Value * 1.0d / rollCount}");
}
Console.ReadLine();
}
public class Dice
{
private readonly Roulette _roulette;
public Dice(Dictionary<int, double> edges)
{
_roulette = CreateRoulette(edges);
}
public int Roll()
{
var sector = _roulette.Roll();
return sector.Id;
}
private static Roulette CreateRoulette(Dictionary<int, double> edges)
{
var sectors = new List<Sector>();
const double onePercentLength = 1.0d / 100;
double sectorFrom = 0;
foreach (var edge in edges)
{
var sectorTo = sectorFrom + edge.Value * onePercentLength;
var sector = new Sector(edge.Key, sectorFrom, sectorTo);
sectors.Add(sector);
sectorFrom = sectorTo;
}
var roulette = new Roulette(sectors);
return roulette;
}
}
public class Sector
{
public double From { get; }
public double To { get; }
public int Id { get; }
public Sector(int id, double from, double to)
{
Id = id;
From = from;
To = to;
}
}
public class Roulette
{
private static readonly Random _random = new Random();
private readonly List<Sector> _sectors;
public Roulette(List<Sector> sectors)
{
_sectors = sectors;
}
public Sector Roll()
{
var r = _random.NextDouble();
foreach (var sector in _sectors)
{
if (IsValueInSector(r, sector))
{
return sector;
}
}
return null;
}
private static bool IsValueInSector(double value, Sector sector)
{
return value >= sector.From && value < sector.To;
}
}
Протестировать, поиграться можно тут
А мне больше всего нравится простота и "дубовость" ответа Vladimir Gamalyan.
Я решил развить его идею до следующего кода:
private var dist = new[]{
//цифра кубика, возможность выпадения
Enumerable.Repeat(1, 10),
Enumerable.Repeat(2, 15),
Enumerable.Repeat(3, 40),
Enumerable.Repeat(4, 5),
Enumerable.Repeat(5, 10),
Enumerable.Repeat(6, 20),
}
.SelectMany(a => a).ToArray();
private var random = new Random();
public int Roll() {
return dist[random.Next(dist.Length)];
}
Как следствие: код простой, дубовый, легко читается, легко правится, и, вероятно, самый быстро работающий из всех предложенных.
Еще из плюсов кода - можно развить возможность выпадения и до сотой процента легким движением руки:
Enumerable.Repeat(1, 1.11 * 100), //1,11%
Enumerable.Repeat(2, 15 * 100), //15%
