Improve chance simulator forecasts and tracking UI
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
1d278ff551
commit
6e64cfacd5
10 changed files with 534 additions and 101 deletions
|
|
@ -327,13 +327,14 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.deleteTitle",
|
||||
"toolboxes.modules.rngSimulator.noActiveRateTitle",
|
||||
"toolboxes.modules.rngSimulator.activeRateLabel",
|
||||
"toolboxes.modules.rngSimulator.commonSettingsTitle",
|
||||
"toolboxes.modules.rngSimulator.effectiveRateLabel",
|
||||
"toolboxes.modules.rngSimulator.statsTabLabel",
|
||||
"toolboxes.modules.rngSimulator.forecastsTabLabel",
|
||||
"toolboxes.modules.rngSimulator.simulationTabLabel",
|
||||
"toolboxes.modules.rngSimulator.trackingTabLabel",
|
||||
"toolboxes.modules.rngSimulator.simulationDrawsLabel",
|
||||
"toolboxes.modules.rngSimulator.perHourLabel",
|
||||
"toolboxes.modules.rngSimulator.perDayLabel",
|
||||
"toolboxes.modules.rngSimulator.perMonthLabel",
|
||||
"toolboxes.modules.rngSimulator.multiplierLabel",
|
||||
"toolboxes.modules.rngSimulator.averageDrawsLabel",
|
||||
"toolboxes.modules.rngSimulator.simulationChanceLabel",
|
||||
|
|
@ -345,14 +346,14 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.occurrenceCountLabel",
|
||||
"toolboxes.modules.rngSimulator.hourTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.dayTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.monthTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.durationChancesTitle",
|
||||
"toolboxes.modules.rngSimulator.durationChancePrefix",
|
||||
"toolboxes.modules.rngSimulator.chanceTargetsTitle",
|
||||
"toolboxes.modules.rngSimulator.chanceLabel",
|
||||
"toolboxes.modules.rngSimulator.costLabel",
|
||||
"toolboxes.modules.rngSimulator.drawsUnit",
|
||||
"toolboxes.modules.rngSimulator.hoursUnit",
|
||||
"toolboxes.modules.rngSimulator.daysUnit",
|
||||
"toolboxes.modules.rngSimulator.monthsUnit",
|
||||
"toolboxes.modules.rngSimulator.emptySummary",
|
||||
"toolboxes.modules.rngSimulator.simulationTitle",
|
||||
"toolboxes.modules.rngSimulator.simulateButton",
|
||||
|
|
@ -370,6 +371,24 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.luckySimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.neutralSimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.unluckySimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.realDrawsLabel",
|
||||
"toolboxes.modules.rngSimulator.realCumulativeChanceLabel",
|
||||
"toolboxes.modules.rngSimulator.afterDrawsPrefix",
|
||||
"toolboxes.modules.rngSimulator.averagePositionLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusStartLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusFarBelowLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusBelowLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusAverageLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusSlightlyAboveLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusHighLabel",
|
||||
"toolboxes.modules.rngSimulator.averageStatusCatastrophicLabel",
|
||||
"toolboxes.modules.rngSimulator.drawsUntilAverageLabel",
|
||||
"toolboxes.modules.rngSimulator.averageExceededLabel",
|
||||
"toolboxes.modules.rngSimulator.averageCostDeltaLabel",
|
||||
"toolboxes.modules.rngSimulator.trackingTargetsTitle",
|
||||
"toolboxes.modules.rngSimulator.trackingProgressLabel",
|
||||
"toolboxes.modules.rngSimulator.remainingDrawsLabel",
|
||||
"toolboxes.modules.rngSimulator.remainingCostLabel",
|
||||
"toolboxes.modules.rngSimulator.importPlaceholder",
|
||||
"toolboxes.modules.rngSimulator.importOpenButton",
|
||||
"toolboxes.modules.rngSimulator.exportOpenButton",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, l
|
|||
import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js";
|
||||
import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js";
|
||||
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
|
||||
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngSummary, getEffectiveRate, normalizeCostString, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
|
||||
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngRealDrawsSummary, calculateRngSummary, getAveragePositionStatus, getDurationPresetsForAverageHours, getEffectiveRate, normalizeCostString, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
|
||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||
|
|
@ -300,8 +300,8 @@ test("rng simulator preserves rate precision and compacts storage", () => {
|
|||
const normalized = normalizeRngSimulatorData({
|
||||
activeRateId: "rate2",
|
||||
rates: [
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "", multiplier: 25, occurrenceValue: 10, occurrenceUnit: "perHour" },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "12,500", multiplier: 50, occurrenceValue: 120, occurrenceUnit: "perDay" },
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "", multiplier: 25, occurrenceValue: 10, occurrenceUnit: "perHour", realDraws: 0 },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "12,500", multiplier: 50, occurrenceValue: 3600, occurrenceUnit: "perMonth", realDraws: "100 000" },
|
||||
{ id: "bad", label: "", rate: "4" }
|
||||
],
|
||||
settings: { simulationDraws: 2000 },
|
||||
|
|
@ -313,7 +313,10 @@ test("rng simulator preserves rate precision and compacts storage", () => {
|
|||
assert.equal(normalized.activeRateId, "rate2");
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.label, rate.rate]), [["Drop rare", "2.500"], ["Ultra rare", "0.000004"]]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => rate.costPerDraw), ["", "12.500"]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.multiplier, rate.occurrenceValue, rate.occurrenceUnit]), [[25, 10, "perHour"], [50, 120, "perDay"]]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.multiplier, rate.occurrenceValue, rate.occurrenceUnit, rate.realDraws]), [[25, 10, "perHour", 0], [50, 120, "perDay", 100000]]);
|
||||
assert.equal(normalizeRngSimulatorData({ rates: [{ label: "Bad real", rate: "5", realDraws: -10 }] }).rates[0].realDraws, 0);
|
||||
assert.equal(normalizeRngSimulatorData({ rates: [{ label: "Decimal real", rate: "5", realDraws: "42.9" }] }).rates[0].realDraws, 42);
|
||||
assert.equal(normalizeRngSimulatorData({ rates: [{ label: "Huge real", rate: "5", realDraws: 2000000000 }] }).rates[0].realDraws, 1000000000);
|
||||
assert.equal(normalized.history, undefined);
|
||||
assert.equal(normalizeCostString("0"), "");
|
||||
assert.equal(normalizeCostString("0012,500"), "12.500");
|
||||
|
|
@ -324,6 +327,8 @@ test("rng simulator preserves rate precision and compacts storage", () => {
|
|||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].multiplier, 50);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].occurrenceValue, 120);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].occurrenceUnit, "perDay");
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[0].realDraws, undefined);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].realDraws, 100000);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).settings.simulationDraws, 2000);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).history, undefined);
|
||||
});
|
||||
|
|
@ -339,10 +344,23 @@ test("rng simulator calculates probability milestones and deterministic simulati
|
|||
const hourlySummary = calculateRngSummary({ rate: "25", simulationDraws: 8, occurrenceValue: 2, occurrenceUnit: "perHour" });
|
||||
assert.equal(hourlySummary.timeByFrequency.hour, 2);
|
||||
assert.equal(hourlySummary.timeByFrequency.day, 2 / 24);
|
||||
assert.equal(hourlySummary.timeByFrequency.month, 2 / 24 / 30);
|
||||
assert.deepEqual(hourlySummary.durationChances.map((preset) => preset.id), ["1h", "5h", "10h", "1j"]);
|
||||
assert.equal(hourlySummary.durationChances.find((preset) => preset.id === "1h").draws, 2);
|
||||
assert.equal(hourlySummary.durationChances.find((preset) => preset.id === "1h").chance, 0.4375);
|
||||
const dailySummary = calculateRngSummary({ rate: "25", simulationDraws: 8, occurrenceValue: 2, occurrenceUnit: "perDay" });
|
||||
assert.equal(dailySummary.timeByFrequency.hour, 48);
|
||||
assert.equal(dailySummary.timeByFrequency.day, 2);
|
||||
assert.deepEqual(dailySummary.durationChances.map((preset) => preset.id), ["1j", "3j", "7j", "30j"]);
|
||||
const legacyMonthSummary = calculateRngSummary({ rate: "25", simulationDraws: 8, occurrenceValue: 60, occurrenceUnit: "perMonth" });
|
||||
assert.equal(legacyMonthSummary.timeByFrequency.day, 2);
|
||||
assert.deepEqual(legacyMonthSummary.durationChances.map((preset) => preset.id), ["1j", "3j", "7j", "30j"]);
|
||||
const fractionalDurationSummary = calculateRngSummary({ rate: "24", simulationDraws: 8, occurrenceValue: 1, occurrenceUnit: "perDay" });
|
||||
assert.ok(fractionalDurationSummary.durationChances.find((preset) => preset.id === "1j").chance > 0);
|
||||
const rareDurationSummary = calculateRngSummary({ rate: "0.000004", simulationDraws: 8, occurrenceValue: 60, occurrenceUnit: "perHour" });
|
||||
assert.deepEqual(rareDurationSummary.durationChances.map((preset) => preset.id), ["1a", "5a", "10a", "25a"]);
|
||||
assert.deepEqual(getDurationPresetsForAverageHours(24 * 30).map((preset) => preset.id), ["1j", "3j", "7j", "30j"]);
|
||||
assert.deepEqual(getDurationPresetsForAverageHours(24 * 365 * 2).map((preset) => preset.id), ["30j", "90j", "1a", "5a"]);
|
||||
assert.deepEqual(getDurationPresetsForAverageHours(0, "perDay").map((preset) => preset.id), ["1j", "3j", "7j", "15j"]);
|
||||
const multipliedSummary = calculateRngSummary({ rate: "10", multiplier: 100, simulationDraws: 1, occurrenceValue: 0, occurrenceUnit: "perHour" });
|
||||
assert.equal(multipliedSummary.effectiveRate, 20);
|
||||
assert.equal(multipliedSummary.averageDraws, 5);
|
||||
|
|
@ -354,6 +372,25 @@ test("rng simulator calculates probability milestones and deterministic simulati
|
|||
assert.equal(costSummary.simulationCostPerSuccess, 50);
|
||||
assert.equal(calculateRngCostSummary({ costPerDraw: "12,5", averageDraws: 5, drawsForChance: {}, simulationDraws: 8, simulationSuccesses: 2, firstSuccessDraw: 3 }).simulationFirstSuccessCost, 37.5);
|
||||
assert.equal(calculateRngCostSummary({ costPerDraw: "0", averageDraws: 5, drawsForChance: {}, simulationDraws: 8, simulationSuccesses: 2 }), null);
|
||||
const realSummary = calculateRngRealDrawsSummary({ rate: "25", realDraws: 2, averageDraws: 4, drawsForChance: { 50: 3, 90: 9 }, costPerDraw: "10" });
|
||||
assert.equal(realSummary.hasCost, true);
|
||||
assert.equal(realSummary.cumulativeChance, 0.4375);
|
||||
assert.equal(realSummary.noSuccessChance, 0.5625);
|
||||
assert.equal(realSummary.averageRatio, 0.5);
|
||||
assert.deepEqual(realSummary.averagePosition, { id: "below", tone: "positive" });
|
||||
assert.equal(realSummary.drawsUntilAverage, 2);
|
||||
assert.equal(realSummary.spentCost, 20);
|
||||
assert.equal(realSummary.averageSuccessCost, 40);
|
||||
assert.equal(realSummary.averageCostDelta, -20);
|
||||
assert.equal(realSummary.costUntilAverage, 20);
|
||||
assert.equal(realSummary.targets[50].remainingDraws, 1);
|
||||
assert.deepEqual(getAveragePositionStatus(0.1), { id: "start", tone: "positive" });
|
||||
assert.deepEqual(getAveragePositionStatus(0.3), { id: "farBelow", tone: "positive" });
|
||||
assert.deepEqual(getAveragePositionStatus(0.75), { id: "below", tone: "positive" });
|
||||
assert.deepEqual(getAveragePositionStatus(1), { id: "average", tone: "" });
|
||||
assert.deepEqual(getAveragePositionStatus(1.25), { id: "slightlyAbove", tone: "negative" });
|
||||
assert.deepEqual(getAveragePositionStatus(2), { id: "high", tone: "negative" });
|
||||
assert.deepEqual(getAveragePositionStatus(3), { id: "catastrophic", tone: "negative" });
|
||||
|
||||
const result = simulateRngDraws("25", 8, (() => {
|
||||
const values = [0.1, 0.9, 0.2, 0.8, 0.7, 0.1, 0.6, 0.5];
|
||||
|
|
@ -386,8 +423,8 @@ test("rng simulator text exchange round trips rates and settings", () => {
|
|||
const rngSimulator = normalizeRngSimulatorData({
|
||||
activeRateId: "rate2",
|
||||
rates: [
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "150", multiplier: 25, occurrenceValue: 20, occurrenceUnit: "perHour" },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "", multiplier: 50, occurrenceValue: 120, occurrenceUnit: "perDay" }
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "150", multiplier: 25, occurrenceValue: 20, occurrenceUnit: "perHour", realDraws: 60 },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "", multiplier: 50, occurrenceValue: 120, occurrenceUnit: "perDay", realDraws: 0 }
|
||||
],
|
||||
settings: { simulationDraws: 5000 }
|
||||
});
|
||||
|
|
@ -403,6 +440,11 @@ test("rng simulator text exchange round trips rates and settings", () => {
|
|||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.multiplier, 50);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.occurrenceValue, 120);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.occurrenceUnit, "perDay");
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Drop rare")?.realDraws, 60);
|
||||
const legacyImported = importModuleText("rngSimulator", "[rates]\n- Legacy | rate=1 | occurrenceValue=300 | occurrenceUnit=perMonth", context);
|
||||
assert.equal(legacyImported.ok, true);
|
||||
assert.equal(legacyImported.data.rates[0].occurrenceValue, 10);
|
||||
assert.equal(legacyImported.data.rates[0].occurrenceUnit, "perDay");
|
||||
assert.equal(imported.data.settings.simulationDraws, 5000);
|
||||
assert.equal(imported.data.history, undefined);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue