Dealing with ranges of numbers - c#

I need to return a value that corresponds to some weighting for a calculation based on age.
Here's the age ranges and weights:-
21-30: 1.2
31-40: 1.8
41-50: 1.9
and so on (there's no real pattern)
The program needs to take an age as input and then return the weighting (e.g. if age = 35, the return value would be 1.8.
How would this be best achieved?
I could use switch but I'm not sure if it's the best way around this. Is there some other construct or technique I could apply in C# to achieve this that would be more effective and portable/scalable should the weightings change?
One other thing - I can't use a database to store any weightings data - just adding this for info.

Create a Dictionary to define your age ranges and weights. The key should be a Tuple with the min-age and max-age for this range and the value should be your weight:
Dictionary<Tuple<int,int>, double> // minAge, MaxAge -> weight
Then you may loop through all keys to find your weight.
You may create this dictionary from the contents of a table, a XML file, a database, whatever.

We have done something similar in a system here, and we use the concept of storing in a database table the weighting and the lower threshold. Thus all we need to do is to find the record with the highest lower threshold less than the value entered and read the weight.
This simplifies the process and allows for editing and adding and removing the values.

No, as far as I know, there is nothing like a range-structure.
You could use a switch, either in this way, if the ranges are always from x1 to y0
switch((value-1) / 10)
{
case 1: ... break;
case 2: ... break;
}
or, if needed:
switch(value)
{
case 11:
case 12:
case 20: ... break;
case 21: ...
}
Depending on the number of groups you need, you could do checks like
if(value > 10 && <= 20) ...
I don't know any more elegant approach.

If the ranges do not overlap then the best thing to use would be a SortedList where the key is the upper value of the range, and the value is the weight. Additionally you can make the weigth nullable to distinguish the case of not finding an entry. I've added the entry of {20, null} so that if the age is <= 20 you'll get null instead of 1.2.
var rangedWeights = new SortedList<int, float?>
{
{ 20, null }
{ 30, 1.2f },
{ 40, 1.8f },
{ 50, 1.9f }
};
int age = 44;
float? weight = null;
foreach (var kvp in rangedWeights)
{
if (age <= kvp.Key)
{
weight = kvp.Value;
break;
}
}
You can dynamically add new entries and still be sure they are sorted.
rangedWeights.Add(60, 2.1f);

You can use dictionary with composite key, which you will use to check the user input and get the respected value for the matching key.
Here is example;
Dictionary<Tuple<int, int>, double> t = new Dictionary<Tuple<int, int>, double>();
t.Add(new Tuple<int,int>(11,20),1f);
t.Add(new Tuple<int, int>(21, 30), 2f);
t.Add(new Tuple<int, int>(31, 40), 3f);
int weight = 34;
double rr = (from d in t where d.Key.Item1 <= weight && d.Key.Item2 >= weight select d.Value).FirstOrDefault();
Console.WriteLine(rr); // rr will print 3.0
Hope it helps..!!

If the ranges are consecutive as they appear to be in your example, you only need the upper value and the ranges sorted in order to be able to query it, so you can do something like this:
public class RangeEntry
{
public RangeEntry(int upperValue, float weight)
{
UpperValue = uperValue;
Weight = weight;
}
public int UpperValue { get; set; }
public float Weight { get; set; }
}
public class RangeWeights
{
private List<RangeEntry> _ranges = new List<RangeEntry>
{
new RangeEntry(30, 1.2f),
new RangeEntry(40, 1.8f),
new RangeEntry(50, 1.9f),
}
public float GetWeight(int value)
{
// If you pre-sort the ranges then you won't need the below OrderBy
foreach (var r in _ranges.OrderBy(o => o.UpperValue))
{
if (value <= r.UpperValue)
return r.Weight;
}
// Range not found, do whatever you want here
throw new InvalidOperationException("value not within in any valid range");
}
}
The value of this approach is that adding a new range means adding just 1 line of code in the instantiation of the ranges.

If you are looking for a shorter way of writing it, I suggest you use ?: operator.
double getWeight(int age){
return (age <= 30) ? 1.2 :
(age <= 40) ? 1.8 : 1.9;
}
It will be the same as using switch. Only shorter way of putting it. You can replace the digits and weights with variables if you don't want them to be hard coded.

It's not very clear what is the structure of the age ranges and weights data,
but I would probably do something like this:
Class AgeRangeAndWeight {
int FromAge {get;set;}
int ToAge {get;set;}
double Weight {get;set;}
}
Class AgeRangeAndWeight Collection() : List<AgeRangeAndWeight> {
AgeRangeAndWeight FindByAge(int age) {
foreach(AgeRangeAndWeight araw in this) {
if(age >= araw.FromAge && age <= araw.ToAge) {
return araw;
}
}
return null;
}
}
then all you have to do is call the FindByAge method. remember to check that it doesn't return null.
Update
Five years after I've posted this answer it was upvoted.
Today I wouldn't recommend inheriting a List<T> - but simply use it's Find method like this:
var list = new List<AgeRangeAndWeight>() {/* populate here */}
var age = 35;
var ageRangeAndWeight = list.Find(a => d.FromAge >= age && d.ToAge <= age);

Related

Ranking sums from first to third c#

I want to rank the number i added up from first to third but i cant think of a way to rank it properly since when there is a duplicate it will only show the number once and continues to the second highest
im new to the language and it would be great for someone to help me on this
Edit: Sorry i think there is a misunderstanding here my sums are in an array that is connected to the names in another array and im trying to sort it out with the same index value
Edit 2: Also i am stuck at c# 7.3 so i cant use some of the new codes
int first = Int32.MinValue;
int fs, nd, thr;
int temp = 0;
for (fs = 0; fs < hounds; fs++)
{
if (score_SUM[fs] > first)
{
first = score_SUM[fs];
temp = fs;
}
}
Console.WriteLine("\n" + "First:{1} {0}", first, houndname[temp]);
int second = Int32.MinValue;
for (nd = 0; nd < hounds; nd++)
{
if (score_SUM[nd] > second && score_SUM[nd] < first)
{
second = score_SUM[nd];
temp = nd;
}
}
Console.WriteLine("Second:{1} {0}", second, houndname[temp]);
int third = Int32.MinValue;
for (thr = 0; thr < hounds; thr++)
{
if (score_SUM[thr] > third && score_SUM[thr] < second)
{
third = score_SUM[thr];
temp = thr;
}
}
Console.WriteLine("Third:{1} {0}", third, houndname[temp]);
Console.ReadLine();
example
10 , 5 , 10 , 6, 1
The output will be like
10
6
5
But I expected
10
10
6
but i cant find a way to write a block a code for that
You're drastically over-engineering this.
If what you have is an array (or list/collection of some kind) of values then you can simply sort that array (descending in this case) and display the first three values.
For example, consider this list of values:
var hounds = new List<int> { 10, 5, 10, 6, 1 };
Then you can sort that list:
hounds = hounds.OrderByDescending(h => h).ToList();
And, either in a loop or by directly referencing the first three (if you know there will always be at least three), output them. For example:
Console.WriteLine("First:{0}", hounds[0]);
Console.WriteLine("Second:{0}", hounds[1]);
Console.WriteLine("Third:{0}", hounds[2]);
Regarding your edit...
my sums are in an array that is connected to the names in another array and im trying to sort it out with the same index value
You're doing it wrong.
Instead of trying to manually keep multiple arrays of values synchronized, maintain one array of meaningful objects. For example, consider how you define a "hound":
public class Hound
{
public int Value { get; set; }
public string Name { get; set; }
}
Create your list of hounds, not multiple lists of disconnected and unrelated values that you need to manually remember and keep synchronized. For example:
var hounds = new List<Hound>
{
new Hound { Value = 10, Name = "Fido" },
new Hound { Value = 5, Name = "Barney" },
new Hound { Value = 10, Name = "Jake" },
new Hound { Value = 6, Name = "Woof" },
new Hound { Value = 1, Name = "Dog" }
};
The rest of the process is the same. Sort the list:
hounds = hounds.OrderByDescending(h => h.Value);
And output the data:
Console.WriteLine("First:{1} {0}", hounds[0].Value, hounds[0].Name);
Console.WriteLine("Second:{1} {0}", hounds[1].Value, hounds[1].Name);
Console.WriteLine("Third:{1} {0}", hounds[2].Value, hounds[2].Name);
Overall, the main point here is that you don't need a ton of convoluted logic just to get the top 3 values in a list. Sorting is a common and well-established operation. All you need is the right data structure to be sorted.
Or, as usual, someone else has already said it better before...
"Smart data structures and dumb code works a lot better than the other way around."
Eric S. Raymond, The Cathedral & the Bazaar
Does this answer your question?
List<int> list = new() { 10, 5, 10, 6, 1 };
list.Sort((x, y) => y.CompareTo(x));
for (int i = 0; i < 3; i++)
{
Console.WriteLine(list[i]);
}
If you only want the three highest values you can also do this:
List<int> list = new() { 10, 5, 10, 6, 1 };
IEnumerable<int> highestValues = list.OrderByDescending(x => x).Take(3);
foreach (int value in highestValues)
{
Console.WriteLine(value);
}
Just change your < symbols to <= symbols. So your checks for the second and third ranks would look something like this:
// check for second
if (score_SUM[nd] > second && score_SUM[nd] <= first)
...
// check for third
if (score_SUM[thr] > third && score_SUM[thr] <= second)
...

How to change a value in C# ConcurrentQueue

I would like to change/set one of the values in the ConcurrentQueue. FixedSizedQueue is a ConcurrentQueue. I think the main issue I'm having to trying to get the ONE of the entries in this ConcurrentQueue. Any suggestions...
private void CalculateABC(FixedSizedQueue<Bar> q)
{
decimal sum = 0.0m;
foreach (var item in q.Queue)
{
sum = sum + item.close;
}
decimal ABCvalue = decimal.Round(sum / q.Limit, 5);
//I'm trying to set the value HERE.
//((Bar)(q.Queue)Items[19]).ABC = ABCvalue;
}
private void CalculateABC(FixedSizedQueue<Bar> q)
{
decimal sum = 0.0m;
Bar item19 = null;
int index = 0;
foreach (var item in q.Queue)
{
if (index++ == 19)
item19 = item;
sum = sum + item.close;
}
decimal ABCvalue = decimal.Round(sum / q.Limit, 5);
//I'm trying to set the value HERE.
if (item19 != null)
item19.ABC = ABCvalue;
}
It seems to me that you just need to do this:
private void CalculateABC(FixedSizedQueue<Bar> q)
{
q.Queue.Skip(19).First().ABC =
decimal.Round(q.Queue.Sum(x => x.close) / q.Limit, 5);
}
Obviously you must ensure that your queue has at least 20 elements for this to work.
While you may want to reevaluate using a queue instead of a list (or in this case a ConcurrentQueue instead of a ConcurrentBag) because it won't provide random access (and you'll need to enumerate all previous elements to get to the one you want) it still is an IEnumerable so you can use LINQ to index it, but the performance will be poor as indexing the 1000th element doesn't require going to adress of first element + 1000 but going over each of the previous 999 elements.
In any case the simplest solution if you want to stick with a queue and index it is to replace your example of
queue[19]
with
queue.ElementAt(19)
So the full example would be :
private void CalculateABC(FixedSizedQueue<Bar> q)
{
// Replace your summing of each item's close property with a simple LINQ sum
decimal sum = q.Queue.Sum(item=>item.close);
decimal ABCvalue = decimal.Round(sum / q.Limit, 5);
// No need for any casting, you're already working on generics, it's already a Bar, don't cast a Bar to a Bar
q.Queue.ElementAt(19).ABC = ABCvalue;
}

C# and "Weighted probability" [duplicate]

This question already has answers here:
Random weighted choice
(12 answers)
Closed 6 years ago.
I am looking to pick 1 person from a list of people where each item in the list has a certain "weighting" to it. Let's assume the Person class has the necessary constructor.
public class Person {
public string Name { get; set; }
public float Weighting { get; set; }
}
public List<Person> People = new List<Person>();
People.Add(new Person("Tim", 1.0));
People.Add(new Person("John", 2.0));
People.Add(new Person("Michael", 4.0));
Now, I want to choose a person randomly from this list. But on average I want to pick Michael 4 times more often than Tim. And I want to pick John half (2/4) as often as Michael. And of course, I want to pick Michael double as often as John.
Does that make any sense?
I already have code in place to select people based on percentages. Wouldn't it work if I just multiplied the %chance of them by the weighting provided in this example?
Also, my current system only works with chances of up to 100%, nothing above. Any advice on how to overcome this limitation? I would probably have to scale every chance according to the largest factor in the list?
public static bool Hit(double pct) {
if (rand == null)
rand = new Random();
return rand.NextDouble() * 100 <= pct;
}
Or am I missing something?
You are not keeping percentages to start with.
I would create a random number in the range of 0 to the sum of all Weighting values. Then just walk over the list and check if the value is lower than the current plus own weight.
So:
float r = YourRandomNumber(People.Sum(p => p.Weighting));
float sum = 0;
foreach (Person p in People)
{
if (r < (sum + p.Weighting))
{
// hit
// save the person somewhere
break;
}
else
{
sum += p.Weighting;
}
}

Game Design/theory, Loot Drop Chance/Spawn Rate

I have a very specific and long-winded question for you all. This question is both about programming and game-theory. I recently added spawnable ore to my Turn Based Strategy Game: http://imgur.com/gallery/0F5D5Ij (For those of you that look please forgive the development textures).
Now, onto the enigma that I have been contemplating. In my game, ore is generated each time a new map is created. 0-8 ore nodes are generated per level-creation. I already have this working; except it only generates "Emeraldite" at this point, which brings me to my question.
How would I, the programmer, make it so nodes have specific rarity? Consider this short mockup which is not actually game data:
(Pseudo Chances that a node will be one of the following)
Bloodstone 1 in 100
Default(Empty Node) 1 in 10
Copper 1 in 15
Emeraldite 1 in 35
Gold 1 in 50
Heronite 1 in 60
Platinum 1 in 60
Shadownite 1 in 75
Silver 1 in 35
Soranite 1 in 1000
Umbrarite 1 in 1000
Cobalt 1 in 75
Iron 1 in 15
I want to make it so that a generated node could be, theoretically, any of the above, however, with the odds also considered. I hope that question is clear enough. I have been trying to wrap my head around this, and even tried to write out a few if statements with randoms, however, I keep coming up empty handed.
Basically, I just want you guys to see my issue, and hopefully provide me with some insight on how I could approach this in a dynamic kind of way.
If any clarification is needed, please ask; sorry again if this was convoluted.
(I am adding C# as a tag only because that is the language I am using for this project)
I'd first represent the probability of each loot type as a simple number.
A probability in pure mathematics is conventionally expressed as a floating point number in the range 0 to 1, but for efficiency, you can use integers in any (large enough) range (each value is the 0-1 value multiplied by the maximum (which I'm calling MaxProbability here)).
e.g. Bloodstone (1 in 100) is 1/100 = 0.01, or MaxProbability * (1/100).
Copper (1 in 15) is 1/15 = 0.06667, or MaxProbability * (1/15).
I'm assuming that 'Default (Empty Node)' means the probability of none of the others.
In this case, the simplest way is not to define it - you get it if none of the others are chosen.
If 'Default' was included, the sum of all these probabilities would be 1 (i.e. 100%) (or MaxProbability, if using integers).
The 1/10 probability of 'Default' in your example is actually a contradiction because the total of all those probabilities is not 1 (it's 0.38247619 - the sum of the probability as calculated in my examples above).
Then you would choose a random number in the range 0 to 1 (or MaxProbability if using integers), and the chosen loot type is the first one in the list such that the sum of the probabilities of it and all previous ones ("cumulative probability") is greater than the random number.
e.g.
MaxProbability = 1000 (I'm using this to make it easy to read).
(For accurate probabilities, you could use 0x7FFFFFFF).
Type Probability Cumulative
---- ----------- ----------
Bloodstone 10 10 (0..9 yield Bloodstone)
Copper 67 77 (10+67) (10..76 yield Copper)
Emeraldite 29 105 (77+29)
Gold 20 125 etc.
Heronite 17 142
Platinum 17 159
Shadownite 13 172
Silver 29 200
Soranite 1 201
Umbrarite 1 202
Cobalt 13 216
Iron 67 282
Default (Empty Node) 7175 1000 (anything else)
e.g. If your random number in the range 0 to 999 (inclusive) was 184 (or anything in the range 172 to 199), you would choose "Silver" (the first one with cumulative probability greater than this).
You could hold the cumulative probabilities in an array and loop through it until you find one higher than the random number, or reach the end.
The order of the list does not matter.
You chose a random number only once per instance.
Including 'Default (Empty Node)' in the list means that the last cumulative probability will always be MaxProbability and the loop that searches it would never go past the end. (Alternatively, 'Default' can be omitted, and you choose it if the loop reaches the end of the list.)
Note that choosing a random number for each one in turn, e.g. a 1/10 chance of 'Bloodstone', then a 1/15 chance of Copper if not Bloodstone, skews the probabilities towards the earlier items:
The actual probability of Copper would be (1/15) * (1 - (1/10)) - 10% less than 1/15.
Here's code to do it (the actual choosing is 5 statements - in the method Choose ).
using System;
namespace ConsoleApplication1
{
class LootChooser
{
/// <summary>
/// Choose a random loot type.
/// </summary>
public LootType Choose()
{
LootType lootType = 0; // start at first one
int randomValue = _rnd.Next(MaxProbability);
while (_lootProbabilites[(int)lootType] <= randomValue)
{
lootType++; // next loot type
}
return lootType;
}
/// <summary>
/// The loot types.
/// </summary>
public enum LootType
{
Bloodstone, Copper, Emeraldite, Gold, Heronite, Platinum,
Shadownite, Silver, Soranite, Umbrarite, Cobalt, Iron, Default
};
/// <summary>
/// Cumulative probabilities - each entry corresponds to the member of LootType in the corresponding position.
/// </summary>
protected int[] _lootProbabilites = new int[]
{
10, 77, 105, 125, 142, 159, 172, 200, 201, 202, 216, 282, // (from the table in the answer - I used a spreadsheet to generate these)
MaxProbability
};
/// <summary>
/// The range of the probability values (dividing a value in _lootProbabilites by this would give a probability in the range 0..1).
/// </summary>
protected const int MaxProbability = 1000;
protected Random _rnd = new Random((int)(DateTime.Now.Ticks & 0x7FFFFFFF));
/// <summary>
/// Simple 'main' to demonstrate.
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
var chooser = new LootChooser();
for(int n=0; n < 100; n++)
Console.Out.WriteLine(chooser.Choose());
}
}
}
You could rewrite all chances so that they use the same divisor (e.g. 1000), your chances then become
Bloodstone 10 in 1000
Default(Empty Node) 100 in 1000
Gold 20 in 1000
Next, create an array of a 1000 elements, and fill it with
10 Bloodstone elements,
100 Empty elements,
20 Gold elements,
etc.
Finally, generate a random number between 0 and 1000, and use that as the index into the element array, this will give
you your random element.
You might have to play with the chances a bit, since you'll probably want all 1000 array elements to be filled, but this is the general idea.
edit its not the most efficient implementation (at least in terms of memory usage, its running time should be good), but I chose this since it allows for a concise explanation that doesn't require a whole lot of math.
First of all, specifying the the default-empty node's probability is unnecessary. The other probabilities should be defined in such a way, that the empty node is created if no other type is created.
How to do this and ensure the generation probabiltiies are equal to those you specified? In short:
convert the probabilities to a floating point (it's a value with a common divisor of 1)
sum all probabilities and check if they are < 1
write a class which will store the all the probabilities
write a function which will get a random node based on those probabilities
For your example:
Bloodstone 1 in 100 = 0.01
Copper 1 in 15 ~= 0.07
Emeraldite 1 in 35 ~= 0.03
Gold 1 in 50 = 0.02
Default = 0.87
Now the class can be implemented in at least two ways. My option consumes much memory, does the computations once, but it also rounds the probability values which may introduce some error. Note, that the error depends on the arrSize variable - the larger it is, the smaller the error.
The other option is as in Bogusz's answer. It is more precise, but required more operations per each generated element.
Option suggested by Thomas requires a lot of repeatable code for each option hence is not versatile. Shellshock's answer will have invalid effective probabilities.
Astrotrain's idea to force yourself to use the same divisor is virtually the same as my own, though the implementation would be slightly different.
Here's a sample implementation of my idea (in java, but should be ported very easily):
public class NodeEntry {
String name;
double probability;
public NodeEntry(String name, double probability) {
super();
this.name = name;
this.probability = probability;
}
public NodeEntry(String name, int howMany, int inHowMany) {
this.name = name;
this.probability = 1.0 * howMany / inHowMany;
}
public final String getName() {
return name;
}
public final void setName(String name) {
this.name = name;
}
public final double getProbability() {
return probability;
}
public final void setProbability(double probability) {
this.probability = probability;
}
#Override
public String toString() {
return name+"("+probability+")";
}
static final NodeEntry defaultNode = new NodeEntry("default", 0);
public static final NodeEntry getDefaultNode() {
return defaultNode;
}
}
public class NodeGen {
List<NodeEntry> nodeDefinitions = new LinkedList<NodeEntry>();
public NodeGen() {
}
public boolean addNode(NodeEntry e) {
return nodeDefinitions.add(e);
}
public boolean addAllNodes(Collection<? extends NodeEntry> c) {
return nodeDefinitions.addAll(c);
}
static final int arrSize = 10000;
NodeEntry randSource[] = new NodeEntry[arrSize];
public void compile() {
checkProbSum();
int offset = 0;
for (NodeEntry ne: nodeDefinitions) {
int amount = (int) (ne.getProbability() * arrSize);
for (int a=0; a<amount;a++) {
randSource[a+offset] = ne;
}
offset+=amount;
}
while (offset<arrSize) {
randSource[offset] = NodeEntry.getDefaultNode();
offset++;
}
}
Random gen = new Random();
public NodeEntry getRandomNode() {
return randSource[gen.nextInt(arrSize)];
}
private void checkProbSum() {
double sum = 0;
for (NodeEntry ne: nodeDefinitions) {
sum+=ne.getProbability();
}
if (sum >1) {
throw new RuntimeException("nodes probability > 1");
}
}
public static void main(String[] args) {
NodeGen ng = new NodeGen();
ng.addNode(new NodeEntry("Test 1", 0.1));
ng.addNode(new NodeEntry("Test 2", 0.2));
ng.addNode(new NodeEntry("Test 3", 0.2));
ng.compile();
Map<NodeEntry, Integer> resCount = new HashMap<NodeEntry, Integer>();
int generations = 10000;
for (int a=0; a<generations; a++) {
NodeEntry node = ng.getRandomNode();
Integer val = resCount.get(node);
if (val == null) {
resCount.put(node, new Integer(1));
} else {
resCount.put(node, new Integer(val+1));
}
}
for (Map.Entry<NodeEntry, Integer> entry: resCount.entrySet()) {
System.out.println(entry.getKey()+": "+entry.getValue()+" ("+(100.0*entry.getValue()/generations)+"%)");
}
}
}
This makes sure the probabilities are actually uniform. If you checked for the first node spawn, then the other, then the other - you would get wrong results: nodes checked first would have increased probability.
Sample run:
Test 2(0.2): 1975 (19.75%)
Test 1(0.1): 1042 (10.42%)
Test 3(0.2): 1981 (19.81%)
default(0.0): 5002 (50.02%)
I think that it is easy to understand how it works.
(Cobalt, 20: means 1 of 20 -> 5%)
Dictionary<string, double> ore = new Dictionary<string, double>();
Random random = new Random();
private void AddOre(string Name, double Value)
{
ore.Add(Name, 1.0 / Value);
}
private string GetOreType()
{
double probSum = 0;
double rand = random.NextDouble();
foreach (var pair in ore)
{
probSum += pair.Value;
if (probSum >= rand)
return pair.Key;
}
return "Normal Ore"; //Reaches this point only if an error occurs.
}
private void Action()
{
AddOre("Cobalt", 20);
AddOre("Stone", 10);
AddOre("Iron", 100);
AddOre("GreenOre", 300);
//Add Common ore and sort Dictionary
AddOre("Common ore", 1 / (1 - ore.Values.Sum()));
ore = ore.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
Console.WriteLine(GetOreType());
}
Edit:
I add section "Add Common ore and sort Dictionary".
I recently had to do something similar, and I ended up with this generic "spawn generator".
public interface ISpawnable : ICloneable
{
int OneInThousandProbability { get; }
}
public class SpawnGenerator<T> where T : ISpawnable
{
private class SpawnableWrapper
{
readonly T spawnable;
readonly int minThreshold;
readonly int maxThreshold;
public SpawnableWrapper(T spawnable, int minThreshold)
{
this.spawnable = spawnable;
this.minThreshold = minThreshold;
this.maxThreshold = this.minThreshold + spawnable.OneInThousandProbability;
}
public T Spawnable { get { return this.spawnable; } }
public int MinThreshold { get { return this.minThreshold; } }
public int MaxThreshold { get { return this.maxThreshold; } }
}
private ICollection<SpawnableWrapper> spawnableEntities;
private Random r;
public SpawnGenerator(IEnumerable<T> objects, int seed)
{
Debug.Assert(objects != null);
r = new Random(seed);
var cumulativeProbability = 0;
spawnableEntities = new List<SpawnableWrapper>();
foreach (var o in objects)
{
var spawnable = new SpawnableWrapper(o, cumulativeProbability);
cumulativeProbability = spawnable.MaxThreshold;
spawnableEntities.Add(spawnable);
}
Debug.Assert(cumulativeProbability <= 1000);
}
//Note that it can spawn null (no spawn) if probabilities dont add up to 1000
public T Spawn()
{
var i = r.Next(0, 1000);
var retVal = (from s in this.spawnableEntities
where (s.MaxThreshold > i && s.MinThreshold <= i)
select s.Spawnable).FirstOrDefault();
return retVal != null ? (T)retVal.Clone() : retVal;
}
}
And you'd use it like:
public class Gem : ISpawnable
{
readonly string color;
readonly int oneInThousandProbability;
public Gem(string color, int oneInThousandProbability)
{
this.color = color;
this.oneInThousandProbability = oneInThousandProbability;
}
public string Color { get { return this.color; } }
public int OneInThousandProbability
{
get
{
return this.oneInThousandProbability;
}
}
public object Clone()
{
return new Gem(this.color, this.oneInThousandProbability);
}
}
var RedGem = new Gem("Red", 250);
var GreenGem = new Gem("Green", 400);
var BlueGem = new Gem("Blue", 100);
var PurpleGem = new Gem("Purple", 190);
var OrangeGem = new Gem("Orange", 50);
var YellowGem = new Gem("Yellow", 10);
var spawnGenerator = new SpawnGenerator<Gem>(new[] { RedGem, GreenGem, BlueGem, PurpleGem, OrangeGem, YellowGem }, DateTime.Now.Millisecond);
var randomGem = spawnGenerator.Spawn();
Evidently the spawn algorithm was not considered critical code so the overhead of this implementation was of no concern when compared to the ease of use. Spawns were run on world creation and it was easily more than fast enough.
Astrotrain already gave my answer but since I coded it up already I'll post it. Sorry for the syntax, I work mostly in Powershell and that is the context currently in my mind. Consider this psuedo code:
// Define the odds for each loot type
// Description,Freq,Range
LootOddsArray = "Bloodstone",1,100,
"Copper",1,15,
"Emeraldite,"1,35,
"Gold",1,50,
"Heronite",1,60,
"Platinum",1,60,
"Shadownite",1,75,
"Silver",1,35,
"Soranite",1,1000,
"Umbrarite",1,1000,
"Cobalt",1,75,
"Iron",1,15
// Define your lookup table. It should be as big as your largest odds range.
LootLookupArray(1000)
// Fill all the 'default' values with "Nothing"
for (i=0;i<LootLookupArray.length;i++) {
LootOddsArray(i) = "Nothing"
}
// Walk through your various treasures
for (i=0;i<LootOddsArray.length;i++)
// Calculate how often the item will appear in the table based on the odds
// and place that many of the item in random places in the table, not overwriting
// any other loot already in the table
NumOccsPer1000 = Round(LootOddsArray(i).Freq * 1000/LootOddsArray(i).Range)
for (l=0;l<NumOccsPer1000;l++) {
// Find an empty slot for the loot
do
LootIndex = Random(1000)
while (LootLookupArray(LootIndex) != "Nothing")
// Array(Index) is empty, put loot there
LootLookupArray(LootIndex) = LootOddsArray(i).Description
}
}
// Roll for Loot
Loot = LootLookupArray(Random(1000))
Use Random.Next http://msdn.microsoft.com/en-us/library/2dx6wyd4(v=vs.110).aspx:
Random rnd = new Random();
if (rnd.Next(1, 101) == 1)
// spawn Bloodstone
if (rnd.Next(1, 16) == 1)
// spawn Copper
if (rnd.Next(1, 36) == 1)
// spawn Emeraldite
The minimum value should always be 1, the maximum value is the odds of spawning the item + 1 (minValue is inclusive, maxValue is exclusive). Always test the return value for 1, e.g., for Bloodstone there is a 1 in 100 chance that the randomly generated number is 1. Of course, this uses a pseudo random number generator, which should be good enough for a game.
A slightly different approach to Astrotrains idea would be that instead of an array to use if statements . The upside is then that you need less memory, the downside that it will need more cpu time to calc the value of the node.
Thus:
Random rnd = new Random();
var number = rnd.next(1,1000);
if (number >= 1 && number <10)
{
// empty
}
else
{
if (number >= 10 && number <100)
{
// bloodstone
}
else
{
//......
}
}
Also a downside to this variant comapred to the array variant is that this one takes more place codewise at the location where you use it and is more prone to errors / corrections (try to add something inside it you need to update all variants).
Thus this here is metnioned for completeness sake but the array vairant (memory usage aside) is less prone to the problems that the if variant has.

C# Count occurrences in list

I have a type Card with an int property called Value where Ace = 14, Five = 5, etc.
If I have a list of Cards (5), ie. a hand. What I would like to do is count the number cards where the Value is equal to another card, I.e. Finding 4 of a kind, 3 of a kind, a pair, two pair etc. I'm fairly new to C#/programming, but I believe this is a case for LINQ/Lambda expression? Can someone help me out?
class Card : IComparable<Card>
{
private string name;
private int value;
private string suit;
public int Value
{
get
{
return value;
}
}
<.....>
//Constructor accepts string ex. "AH" and builds value 14, suit "Hearts"
public int CompareTo(Card that)
{
if (this.value > that.value) return -1;
if (this.value == that.value) return 0;
return 1;
}
}
List<Card> HandBuilder = new List<Card>();
HandBuilder.Add(...); //5 Cards
HandBuilder.Count ?? //Help
It's pretty trivial using GroupBy.
var cards = HandBuilder.GroupBy(card => card.Value)
.OrderByDescending(group => group.Count());
To check for four of a kind just see if the first group has four items; to check for three of a kind see if the first group has three items. To check for two pair just see if the first two groups each have two items.

Categories