//due to the use of doubles, sometimes small rounding errors occur and a final prize selection row //that should have a prob lower bound of 0 will have one + or minus 1 x 10-10 or smaller. //we will replace that with true 0 private void AdjustFinalPrizeSelectionRow(PrizeSelectionRow prizeSelectionRow) { if (Math.Abs(prizeSelectionRow.PrizeProbabilityLowerBound) < 0.0000000001) { prizeSelectionRow.PrizeProbabilityLowerBound = 0; } }
//lower indexes are "higher up" in the table, and require larger numbers to hit //the value part of the dictionary (the double) represents the lower bound of the random number needed to hit this relic public IList <PrizeSelectionRow> GetPrizeSelectionTable(IList <PrizeCategorySpecification> prizeCategorySpecifications) { #region validations if (prizeCategorySpecifications == null || !prizeCategorySpecifications.Any()) { throw new ArgumentException("prizeCategorySpecifications must be greater non null and have 1 or more members"); } if (prizeCategorySpecifications.Any(ps => ps.ProbabilityExtentForEntireCategory < 0.0) || prizeCategorySpecifications.Any(ps => ps.ProbabilityExtentForEntireCategory > 1.0)) { throw new ArgumentException($"prizeCategorySpecifications must be between 0 and 1"); } if (prizeCategorySpecifications.Any(ps => ps.PrizeCount <= 0)) { throw new ArgumentException($"PrizeCount must be between greater than 0"); } #endregion int currentMaxPrizeIndex = 0; //this is the point from which we start adding other probability keys to the bottom. double currentProbabilityLowerBound = 1.0; //this is the point from which we start adding other probability ranges to the bottom. IList <PrizeSelectionRow> prizeSelectionTable = new List <PrizeSelectionRow>(); foreach (var prizeCategorySpecification in prizeCategorySpecifications) { double probabilityIncrement = prizeCategorySpecification.ProbabilityExtentForEntireCategory / prizeCategorySpecification.PrizeCount; for (int counter = 0; counter < prizeCategorySpecification.PrizeCount; counter++) { //pullCategoryProbabilityTable.Add(relicIndex + initialMaxRelicKey + 1, initialLowerBound - (probabilityIncrement * (relicIndex + 1))); //we start from the top prize and work down the table PrizeSelectionRow row = new PrizeSelectionRow() { PrizeIndex = counter + currentMaxPrizeIndex + 1, PrizeProbabilityLowerBound = currentProbabilityLowerBound - (probabilityIncrement * (counter + 1)), PrizeCategoryName = prizeCategorySpecification.PrizeCategoryName, PrizeName = prizeCategorySpecification.PrizeNames[counter] }; prizeSelectionTable.Add(row); } currentProbabilityLowerBound = prizeSelectionTable.OrderBy(row => row.PrizeIndex).Last().PrizeProbabilityLowerBound; currentMaxPrizeIndex = prizeSelectionTable.OrderBy(row => row.PrizeIndex).Last().PrizeIndex; } //correct last row for 0 lower bound if that is what is clearly intended: AdjustFinalPrizeSelectionRow(prizeSelectionTable[currentMaxPrizeIndex - 1]); return(prizeSelectionTable); }
//null row = no matching prizes were found private PrizeSelectionRow SelectPrizeFromPrizeTable(IList <PrizeSelectionRow> prizeSelectionTable, double randomNumber) { PrizeSelectionRow selectedPrize = null; //scan table from the top down until we find the first prize whose lower bound is less than or equal to than the provided roll; //the prize we want is the one immediately lower in the table. //top down - assume already sorted!! foreach (PrizeSelectionRow prizeSelectionRow in prizeSelectionTable) { if (prizeSelectionRow.PrizeProbabilityLowerBound <= randomNumber) { selectedPrize = prizeSelectionRow; break; } } return(selectedPrize); }
public IList <PrizeResultRow> SelectPrizes(IList <SelectionDomain> selectionDomains, Random random = null) { #region Validations foreach (var selectionDomain in selectionDomains) { if (selectionDomain.PrizesToSelectFromDomainCount <= 0) { throw new ArgumentException($"PrizesToSelectFromDomainCount for SelectFromDomain {selectionDomain.SelectionDomainName} must be greater than 0"); } if (!_prizeSelectionTableHelper.IsPrizeSelectionTableValid(selectionDomain.PrizeSelectionTable)) { throw new ArgumentException($"PrizeSelectionTable for selectionDomain {selectionDomain.SelectionDomainName} was invalid"); } } #endregion Stopwatch sw = Stopwatch.StartNew(); //Set up variables and structures if (random == null) { random = new Random(); } IList <PrizeNameCategoryPair> uniquePrizeNames = GetUniquePrizeNameCategoryPairs(selectionDomains); int finalPrizeRowCount = uniquePrizeNames.Count(); IList <PrizeResultRow> prizeResultTable = new List <PrizeResultRow>(finalPrizeRowCount); List <IDictionary <int, int> > resultSummariesList = new List <IDictionary <int, int> >(); //perform the actual selection and counting for (int domainCounter = 0; domainCounter < selectionDomains.Count; domainCounter++) { SelectionDomain currentSelectionDomain = selectionDomains[domainCounter]; //for intermediate use because we need to update the prize counts in a performant way. IDictionary <int, int> resultsSummary = _prizeResultsTableHelper.GetEmptyPrizeResultsSummary(currentSelectionDomain.PrizeSelectionTable.Count); for (int counter = 0; counter < currentSelectionDomain.PrizesToSelectFromDomainCount; counter++) { PrizeSelectionRow selectedPrizeRow = SelectPrizeFromPrizeTable(currentSelectionDomain.PrizeSelectionTable, random.NextDouble()); if (selectedPrizeRow != null) { resultsSummary[selectedPrizeRow.PrizeIndex]++; } } resultSummariesList.Add(resultsSummary); } //now that we have the counts of selected prizes by index, we need to translate them back to the more //informative structure of the prize results table //generate table with all the data except counts and indexes prizeResultTable = uniquePrizeNames.Select(psr => new PrizeResultRow() { PrizeCategoryName = psr.CategoryName, PrizeIndex = 0, PrizeName = psr.PrizeName, PrizeSelectedCount = 0 }).ToList(); //put in indexes for (int i = 0; i < finalPrizeRowCount; i++) { prizeResultTable[i].PrizeIndex = i + 1; } //now insert the counts; assumption is that the keys of the generated resultsSummary dictionary must match in number //and start at the same index (1), as the largestSelectionDomain which is used to prepopulate the prizeResultTable for (int domainCounter = 0; domainCounter < selectionDomains.Count; domainCounter++) { //since the resultsSummary dictionaries don't have names embedded, we can only relate the counts //to prize name in context of the associated selection domain. SelectionDomain currentSelectionDomain = selectionDomains[domainCounter]; IDictionary <int, int> resultsSummary = resultSummariesList[domainCounter]; //walk through prizeResultTable one prize at a time, ADDING in counts //walk through non zero resultsSummary one prize at a time, linking to prize name and ADDING in counts for (int item = 0; item < resultsSummary.Count; item++) { if (resultsSummary[item + 1] > 0) { //get prize name from selection domain prizeSelectionTable string prizeName = currentSelectionDomain.PrizeSelectionTable .Where(r => r.PrizeIndex == (item + 1)).Select(r => r.PrizeName).Single(); //now map prizeName back to prizeResultTable PrizeResultRow rowToUpdate = prizeResultTable.Single(p => p.PrizeName == prizeName); rowToUpdate.PrizeSelectedCount += resultsSummary[item + 1]; } } } sw.Stop(); _logger.LogDebug($"finished a selection operation in {sw.ElapsedMilliseconds} milliseconds"); return(prizeResultTable); }