@@ -83,110 +83,222 @@ const formatNumber = (value) => {
8383 return value . toFixed ( 2 ) ;
8484} ;
8585
86- const datapoints = { } ;
87- const maxseq = { } ;
88- const runs = { } ;
8986const totalsdir = __dirname + '/simulation-totals/' ;
9087const graphsdir = __dirname + '/simulation-graphs/' ;
9188
9289ensureDir ( totalsdir ) ;
9390ensureDir ( 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...' ) ;
95254for ( 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
103262for ( 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