Skip to content

Commit 931b2c9

Browse files
Tweaked simulation and precalc handling.
1 parent b75ab87 commit 931b2c9

3 files changed

Lines changed: 285 additions & 104 deletions

File tree

analyze-simulations.js

Lines changed: 197 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -83,110 +83,222 @@ const formatNumber = (value) => {
8383
return value.toFixed(2);
8484
};
8585

86-
const datapoints = {};
87-
const maxseq = {};
88-
const runs = {};
8986
const totalsdir = __dirname + '/simulation-totals/';
9087
const graphsdir = __dirname + '/simulation-graphs/';
9188

9289
ensureDir(totalsdir);
9390
ensureDir(graphsdir);
9491

92+
let datapoints = {};
93+
let runs = {};
94+
let fileCache = new Map();
95+
96+
const generateGraphsForItem = (tc, playermod, itc, data, runcount) => {
97+
const samplesCount = Math.max(5000, data.length * 200);
98+
const binCount = 60;
99+
const dist = buildDistribution(data, samplesCount, binCount);
100+
const safeBase = safeName(`${tc} [${playermod}][${itc}]`);
101+
const totalsPath = totalsdir + safeBase + '.json';
102+
const graphPath = graphsdir + safeBase + '.svg';
103+
104+
const totalsPayload = {
105+
tc,
106+
playermod: Number(playermod),
107+
itc,
108+
runs: runcount,
109+
samples: data.length,
110+
mean: dist.avg,
111+
stddev: dist.deviation,
112+
minX: dist.minX,
113+
maxX: dist.maxX,
114+
minY: dist.minY,
115+
maxY: dist.maxY,
116+
distribution: dist.distribution
117+
};
118+
119+
fs.writeFileSync(totalsPath, JSON.stringify(totalsPayload, null, 2));
120+
121+
const plotWidth = 700;
122+
const plotHeight = 350;
123+
const gutterTop = 40;
124+
const gutterBottom = 30;
125+
const gutterX = 80;
126+
const width = gutterX + plotWidth + gutterX;
127+
const height = gutterTop + plotHeight + gutterBottom;
128+
const boxX = -gutterX;
129+
const boxY = -gutterTop;
130+
const rangeX = dist.maxX - dist.minX || 1;
131+
const rangeY = dist.maxY - dist.minY || 1;
132+
133+
const points = dist.distribution.map((point, index) => {
134+
const x = ((point.x - dist.minX) / rangeX) * plotWidth;
135+
const y = plotHeight - ((point.y - dist.minY) / rangeY) * plotHeight;
136+
return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
137+
}).join(' ');
138+
139+
const svg = [
140+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${boxX} ${boxY} ${width} ${height}">`,
141+
`<rect x="${boxX}" y="${boxY}" width="${width}" height="${height}" fill="#11161c"/>`,
142+
`<rect x="0" y="0" width="${plotWidth}" height="${plotHeight}" fill="#0b0f14" stroke="#2a323d" stroke-width="2"/>`,
143+
`<line x1="0" y1="${plotHeight}" x2="${plotWidth}" y2="${plotHeight}" stroke="#2a323d" stroke-width="2"/>`,
144+
`<line x1="0" y1="0" x2="0" y2="${plotHeight}" stroke="#2a323d" stroke-width="2"/>`,
145+
`<path d="${points}" fill="none" stroke="#8ad1ff" stroke-width="2"/>`,
146+
`<text x="${(width) / 2 - gutterX}" y="${-gutterTop / 2 + 5}" fill="#e6f0ff" font-size="18" text-anchor="middle">${tc} | players ${playermod * 2 - 1} | ${itc} | ${formatNumber(dist.avg)} ± ${formatNumber(dist.deviation)}</text>`,
147+
`<text x="0" y="${plotHeight + gutterBottom / 2 + 5}" fill="#d7e3f2" font-size="14" text-anchor="start">${formatNumber(dist.minX)}</text>`,
148+
`<text x="${plotWidth}" y="${plotHeight + gutterBottom / 2 + 5}" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.maxX)}</text>`,
149+
`<text x="-10" y="${plotHeight + 4}" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.minY)}</text>`,
150+
`<text x="-10" y="4" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.maxY)}</text>`,
151+
`</svg>`
152+
].join('');
153+
154+
fs.writeFileSync(graphPath, svg);
155+
};
156+
157+
const removeOutputsForItem = (tc, playermod, itc) => {
158+
const safeBase = safeName(`${tc} [${playermod}][${itc}]`);
159+
const totalsPath = totalsdir + safeBase + '.json';
160+
const graphPath = graphsdir + safeBase + '.svg';
161+
if (fs.existsSync(totalsPath)) {
162+
fs.unlinkSync(totalsPath);
163+
}
164+
if (fs.existsSync(graphPath)) {
165+
fs.unlinkSync(graphPath);
166+
}
167+
};
168+
169+
const applySimulationData = (data, direction) => {
170+
if (!data || data.runs <= 0) {
171+
return [];
172+
}
173+
174+
const affected = [];
175+
const tc = data.tc;
176+
const playermod = data.playermod;
177+
178+
datapoints[tc] = datapoints[tc] || {};
179+
datapoints[tc][playermod] = datapoints[tc][playermod] || {};
180+
181+
runs[tc] = runs[tc] || {};
182+
runs[tc][playermod] = runs[tc][playermod] || 0;
183+
runs[tc][playermod] += direction * data.runs;
184+
185+
for (let item of data.drops) {
186+
let [itc, count] = item;
187+
datapoints[tc][playermod][itc] = datapoints[tc][playermod][itc] || [];
188+
if (direction > 0) {
189+
datapoints[tc][playermod][itc][data.seq] = count / data.runs;
190+
} else {
191+
delete datapoints[tc][playermod][itc][data.seq];
192+
}
193+
affected.push({ tc, playermod, itc });
194+
}
195+
196+
return affected;
197+
};
198+
199+
const rebuildAffectedItems = (affected) => {
200+
const unique = new Map();
201+
for (let item of affected) {
202+
unique.set(`${item.tc}||${item.playermod}||${item.itc}`, item);
203+
}
204+
205+
for (let item of unique.values()) {
206+
const itemData = datapoints?.[item.tc]?.[item.playermod]?.[item.itc] || [];
207+
const runcount = runs?.[item.tc]?.[item.playermod] || 0;
208+
const hasValues = itemData.some(v => v !== undefined);
209+
210+
if (!hasValues || runcount <= 0) {
211+
removeOutputsForItem(item.tc, item.playermod, item.itc);
212+
continue;
213+
}
214+
215+
const normalized = itemData.map(v => v || 0);
216+
generateGraphsForItem(item.tc, item.playermod, item.itc, normalized, runcount);
217+
}
218+
};
219+
220+
const processSimulationFile = (filePath, options = { rebuild: true }) => {
221+
try {
222+
const data = JSON.parse(fs.readFileSync(filePath));
223+
const previous = fileCache.get(filePath);
224+
let affected = [];
225+
226+
if (previous) {
227+
affected = affected.concat(applySimulationData(previous, -1));
228+
}
229+
230+
affected = affected.concat(applySimulationData(data, 1));
231+
fileCache.set(filePath, data);
232+
233+
if (options.rebuild) {
234+
rebuildAffectedItems(affected);
235+
}
236+
} catch (err) {
237+
console.error(`Error processing file ${filePath}:`, err.message);
238+
}
239+
};
240+
241+
const removeSimulationFile = (filePath) => {
242+
const previous = fileCache.get(filePath);
243+
if (!previous) {
244+
return;
245+
}
246+
247+
const affected = applySimulationData(previous, -1);
248+
fileCache.delete(filePath);
249+
rebuildAffectedItems(affected);
250+
};
251+
252+
// Initial build
253+
console.log('Building initial graphs...');
95254
for (let dir of [totalsdir, graphsdir]) {
96255
for (let file of fs.readdirSync(dir)) {
97256
if (file.endsWith('.json') || file.endsWith('.svg')) {
98-
fs.unlinkSync(dir + file);
257+
fs.unlinkSync(dir + file);
99258
}
100259
}
101260
}
102261

103262
for (let file of fs.readdirSync(__dirname + '/simulations/')) {
104263
if (file.endsWith('.json')) {
105-
const data = JSON.parse(fs.readFileSync(__dirname + '/simulations/' + file));
106-
107-
if (data.runs > 0) {
108-
datapoints[data.tc] = datapoints[data.tc] || {};
109-
datapoints[data.tc][data.playermod] = datapoints[data.tc][data.playermod] || {};
110-
maxseq[data.tc] = maxseq[data.tc] || {};
111-
maxseq[data.tc][data.playermod] = maxseq[data.tc][data.playermod] || 0;
112-
maxseq[data.tc][data.playermod] = Math.max(maxseq[data.tc][data.playermod], data.seq);
113-
114-
runs[data.tc] = runs[data.tc] || {};
115-
runs[data.tc][data.playermod] = runs[data.tc][data.playermod] || 0;
116-
runs[data.tc][data.playermod] += data.runs;
117-
118-
for (let item of data.drops) {
119-
let [itc, count, magic, rare, set, unique] = item;
120-
datapoints[data.tc][data.playermod][itc] = datapoints[data.tc][data.playermod][itc] || [];
121-
datapoints[data.tc][data.playermod][itc][data.seq] = count / data.runs;
122-
}
123-
}
264+
processSimulationFile(__dirname + '/simulations/' + file, { rebuild: false });
124265
}
125266
}
126267

127-
for (let tc in maxseq) {
128-
for (let playermod in maxseq[tc]) {
268+
const allAffected = [];
269+
for (let tc in datapoints) {
270+
for (let playermod in datapoints[tc]) {
129271
for (let itc in datapoints[tc][playermod]) {
130-
let data = datapoints[tc][playermod][itc].map(v => v || 0), runcount = runs[tc][playermod];
131-
132-
const samplesCount = Math.max(5000, data.length * 200);
133-
const binCount = 60;
134-
const dist = buildDistribution(data, samplesCount, binCount);
135-
const safeBase = safeName(`${tc} [${playermod}][${itc}]`);
136-
const totalsPath = totalsdir + safeBase + '.json';
137-
const graphPath = graphsdir + safeBase + '.svg';
138-
139-
const totalsPayload = {
140-
tc,
141-
playermod: Number(playermod),
142-
itc,
143-
runs: runcount,
144-
samples: data.length,
145-
mean: dist.avg,
146-
stddev: dist.deviation,
147-
minX: dist.minX,
148-
maxX: dist.maxX,
149-
minY: dist.minY,
150-
maxY: dist.maxY,
151-
distribution: dist.distribution
152-
};
153-
154-
fs.writeFileSync(totalsPath, JSON.stringify(totalsPayload, null, 2));
155-
156-
const plotWidth = 700;
157-
const plotHeight = 350;
158-
const gutterTop = 40;
159-
const gutterBottom = 30;
160-
const gutterX = 80;
161-
const width = gutterX + plotWidth + gutterX;
162-
const height = gutterTop + plotHeight + gutterBottom;
163-
const boxX = -gutterX;
164-
const boxY = -gutterTop;
165-
const rangeX = dist.maxX - dist.minX || 1;
166-
const rangeY = dist.maxY - dist.minY || 1;
167-
168-
const points = dist.distribution.map((point, index) => {
169-
const x = ((point.x - dist.minX) / rangeX) * plotWidth;
170-
const y = plotHeight - ((point.y - dist.minY) / rangeY) * plotHeight;
171-
return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`;
172-
}).join(' ');
173-
174-
const svg = [
175-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${boxX} ${boxY} ${width} ${height}">`,
176-
`<rect x="${boxX}" y="${boxY}" width="${width}" height="${height}" fill="#11161c"/>`,
177-
`<rect x="0" y="0" width="${plotWidth}" height="${plotHeight}" fill="#0b0f14" stroke="#2a323d" stroke-width="2"/>`,
178-
`<line x1="0" y1="${plotHeight}" x2="${plotWidth}" y2="${plotHeight}" stroke="#2a323d" stroke-width="2"/>`,
179-
`<line x1="0" y1="0" x2="0" y2="${plotHeight}" stroke="#2a323d" stroke-width="2"/>`,
180-
`<path d="${points}" fill="none" stroke="#8ad1ff" stroke-width="2"/>`,
181-
`<text x="${(width) / 2 - gutterX}" y="${-gutterTop / 2 + 5}" fill="#e6f0ff" font-size="18" text-anchor="middle">${tc} | players ${playermod * 2 - 1} | ${itc} | ${formatNumber(dist.avg)} ± ${formatNumber(dist.deviation)}</text>`,
182-
`<text x="0" y="${plotHeight + gutterBottom / 2 + 5}" fill="#d7e3f2" font-size="14" text-anchor="start">${formatNumber(dist.minX)}</text>`,
183-
`<text x="${plotWidth}" y="${plotHeight + gutterBottom / 2 + 5}" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.maxX)}</text>`,
184-
`<text x="-10" y="${plotHeight + 4}" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.minY)}</text>`,
185-
`<text x="-10" y="4" fill="#d7e3f2" font-size="14" text-anchor="end">${formatNumber(dist.maxY)}</text>`,
186-
`</svg>`
187-
].join('');
188-
189-
fs.writeFileSync(graphPath, svg);
272+
allAffected.push({ tc, playermod, itc });
190273
}
191274
}
192275
}
276+
rebuildAffectedItems(allAffected);
277+
console.log('Initial build complete. Watching for changes...');
278+
279+
// Watch for file changes
280+
const THROTTLE_MS = 250;
281+
const lastHandled = new Map();
282+
283+
fs.watch(__dirname + '/simulations/', (eventType, filename) => {
284+
if (!filename || !filename.endsWith('.json')) {
285+
return;
286+
}
287+
288+
const filePath = __dirname + '/simulations/' + filename;
289+
const now = Date.now();
290+
const last = lastHandled.get(filePath) || 0;
291+
if (now - last < THROTTLE_MS) {
292+
return;
293+
}
294+
lastHandled.set(filePath, now);
295+
296+
if (!fs.existsSync(filePath)) {
297+
console.log(`Detected removal of ${filename}, rebuilding...`);
298+
removeSimulationFile(filePath);
299+
return;
300+
}
301+
302+
console.log(`Detected ${eventType} on ${filename}, rebuilding...`);
303+
processSimulationFile(filePath);
304+
});

0 commit comments

Comments
 (0)