@open-rgs/simulator 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,435 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>open-rgs simulation — {{game.id}}</title>
7
+ <meta name="color-scheme" content="light dark" />
8
+ <script>
9
+ (function () {
10
+ try {
11
+ var pref = localStorage.getItem("orgs-sim-theme");
12
+ var sys = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
13
+ document.documentElement.dataset.theme = pref || sys;
14
+ } catch (e) { document.documentElement.dataset.theme = "light"; }
15
+ })();
16
+ </script>
17
+ <style>
18
+ :root {
19
+ --serif: ui-serif, Charter, "Iowan Old Style", "Source Serif 4", "Source Serif Pro", Georgia, serif;
20
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
21
+ --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
22
+ }
23
+ :root[data-theme="light"] {
24
+ --bg: #fbf9f3; --bg-soft: #f3f0e6; --bg-code: #f1ede0;
25
+ --fg: #1a1a1a; --fg-mute: #5a5650; --fg-faint: #8a857c;
26
+ --rule: #d8d3c4; --rule-soft: #ebe6d6;
27
+ --accent: #7a2f2f; --accent-soft: #b56b4a;
28
+ --ok: #4a6b3a; --warn: #b58040; --fail: #a93838;
29
+ }
30
+ :root[data-theme="dark"] {
31
+ --bg: #16140f; --bg-soft: #1d1b16; --bg-code: #1f1d17;
32
+ --fg: #e6e0d0; --fg-mute: #a09a8a; --fg-faint: #706a5e;
33
+ --rule: #2f2c24; --rule-soft: #25221c;
34
+ --accent: #d59879; --accent-soft: #c5775a;
35
+ --ok: #8db876; --warn: #d6a55c; --fail: #d6736e;
36
+ }
37
+ * { box-sizing: border-box; }
38
+ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); }
39
+ body {
40
+ font-family: var(--serif);
41
+ font-size: 16px;
42
+ line-height: 1.55;
43
+ font-feature-settings: "kern", "liga", "onum";
44
+ text-rendering: optimizeLegibility;
45
+ -webkit-font-smoothing: antialiased;
46
+ }
47
+ ::selection { background: var(--accent); color: var(--bg); }
48
+
49
+ main { max-width: 56rem; margin: 0 auto; padding: 3rem 1.5rem 5rem; }
50
+ .bar {
51
+ max-width: 56rem; margin: 0 auto; padding: 1.25rem 1.5rem;
52
+ display: flex; justify-content: space-between; align-items: baseline;
53
+ font-family: var(--sans); font-size: 0.8125rem; color: var(--fg-mute);
54
+ }
55
+ .wordmark { font-family: var(--mono); font-size: 0.875rem; color: var(--fg); }
56
+ #theme-toggle {
57
+ background: none; border: 1px solid var(--rule); color: var(--fg-mute);
58
+ font-family: var(--sans); font-size: 0.75rem; padding: 0.25rem 0.625rem;
59
+ cursor: pointer; border-radius: 2px;
60
+ }
61
+ #theme-toggle:hover { color: var(--fg); border-color: var(--fg-mute); }
62
+ :root[data-theme="dark"] #theme-toggle .theme-dark,
63
+ :root[data-theme="light"] #theme-toggle .theme-light { display: none; }
64
+
65
+ h1 {
66
+ font-family: var(--serif); font-size: 2.25rem; line-height: 1.15;
67
+ margin: 0 0 0.5rem; font-weight: 600; letter-spacing: -0.015em;
68
+ }
69
+ h2 {
70
+ font-family: var(--serif); font-size: 1rem; font-weight: 700;
71
+ margin: 3rem 0 0.5rem; font-variant: small-caps; letter-spacing: 0.08em;
72
+ color: var(--fg); display: flex; align-items: baseline; gap: 0.625rem;
73
+ }
74
+ h2 .num {
75
+ font-family: var(--mono); font-size: 0.75rem; font-variant: normal;
76
+ letter-spacing: 0; color: var(--accent); font-weight: 400;
77
+ }
78
+ h3 {
79
+ font-family: var(--serif); font-size: 1.5rem; line-height: 1.2;
80
+ margin: 4rem 0 0.5rem; font-weight: 600; letter-spacing: -0.01em;
81
+ padding-top: 2rem; border-top: 1px solid var(--rule-soft);
82
+ }
83
+ h3:first-of-type { border-top: none; padding-top: 0; }
84
+ h3 .mode-label { color: var(--fg-mute); font-style: italic; font-weight: 400; font-size: 1rem; margin-left: 0.5rem; }
85
+ p { margin: 0 0 1rem; color: var(--fg); }
86
+ p.tagline { color: var(--fg-mute); font-size: 1rem; }
87
+ p.narrative {
88
+ font-style: italic; color: var(--fg);
89
+ border-left: 2px solid var(--accent); padding: 0.5rem 0 0.5rem 1rem;
90
+ margin: 1.25rem 0;
91
+ }
92
+ .meta {
93
+ font-family: var(--mono); font-size: 0.8125rem; color: var(--fg-mute);
94
+ margin: 0.5rem 0 1.5rem;
95
+ }
96
+ .meta .sep { color: var(--fg-faint); margin: 0 0.5rem; }
97
+
98
+ .stat-grid {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
101
+ gap: 1rem 1.5rem;
102
+ margin: 1.5rem 0 2rem;
103
+ padding: 1rem 0;
104
+ border-top: 1px solid var(--rule-soft);
105
+ border-bottom: 1px solid var(--rule-soft);
106
+ }
107
+ .stat { display: flex; flex-direction: column; gap: 0.125rem; }
108
+ .stat .label {
109
+ font-family: var(--sans); font-size: 0.6875rem; color: var(--fg-mute);
110
+ text-transform: uppercase; letter-spacing: 0.08em;
111
+ }
112
+ .stat .value {
113
+ font-family: var(--serif); font-size: 1.25rem; font-weight: 600;
114
+ letter-spacing: -0.01em; color: var(--fg);
115
+ }
116
+ .stat .value.muted { color: var(--fg-mute); font-weight: 400; font-size: 1rem; }
117
+
118
+ table {
119
+ border-collapse: collapse; width: 100%;
120
+ font-size: 0.9rem; margin: 1rem 0 2rem;
121
+ }
122
+ th, td {
123
+ text-align: left; padding: 0.5rem 0.75rem 0.5rem 0;
124
+ border-bottom: 1px solid var(--rule-soft); vertical-align: top;
125
+ }
126
+ th {
127
+ font-family: var(--sans); font-weight: 600; font-size: 0.75rem;
128
+ color: var(--fg-mute); text-transform: uppercase; letter-spacing: 0.06em;
129
+ border-bottom: 1px solid var(--rule);
130
+ }
131
+ td.mono, th.mono { font-family: var(--mono); font-size: 0.8125rem; }
132
+ td.num { font-family: var(--mono); font-size: 0.8125rem; text-align: right; }
133
+ th.num { text-align: right; }
134
+
135
+ .status {
136
+ display: inline-block; font-family: var(--mono); font-size: 0.7rem;
137
+ padding: 0.125rem 0.5rem; border-radius: 2px;
138
+ font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
139
+ }
140
+ .status-ok { color: var(--ok); border: 1px solid var(--ok); }
141
+ .status-warn { color: var(--warn); border: 1px solid var(--warn); }
142
+ .status-fail { color: var(--fail); border: 1px solid var(--fail); }
143
+
144
+ .delta-pos { color: var(--ok); }
145
+ .delta-neg { color: var(--fail); }
146
+
147
+ .empty { color: var(--fg-faint); font-style: italic; font-size: 0.875rem; }
148
+
149
+ .footer {
150
+ margin-top: 4rem; padding-top: 1.5rem;
151
+ border-top: 1px solid var(--rule-soft);
152
+ font-family: var(--sans); font-size: 0.75rem;
153
+ color: var(--fg-mute);
154
+ display: flex; justify-content: space-between;
155
+ }
156
+
157
+ details summary {
158
+ cursor: pointer; font-family: var(--sans); font-size: 0.8125rem;
159
+ color: var(--fg-mute); padding: 0.5rem 0;
160
+ }
161
+ details[open] summary { color: var(--fg); }
162
+
163
+ @media (max-width: 40rem) {
164
+ main { padding: 1.5rem 1rem 3rem; }
165
+ h1 { font-size: 1.75rem; }
166
+ .stat-grid { grid-template-columns: repeat(2, 1fr); }
167
+ }
168
+ </style>
169
+ </head>
170
+ <body>
171
+
172
+ <header class="bar">
173
+ <span class="wordmark">open-rgs · simulator</span>
174
+ <button id="theme-toggle" type="button" aria-label="Toggle theme">
175
+ <span class="theme-light">☉ light</span>
176
+ <span class="theme-dark">☽ dark</span>
177
+ </button>
178
+ </header>
179
+
180
+ <main>
181
+ <h1>Simulation — {{game.id}}</h1>
182
+ <p class="tagline">
183
+ Declared game RTP <strong>{{pct game.declaredRtp}}</strong> · default mode <code>{{game.defaultMode}}</code>
184
+ </p>
185
+ <p class="meta">
186
+ generated <span title="{{generatedAt}}">{{generatedAt}}</span>
187
+ <span class="sep">·</span> {{generator}}
188
+ <span class="sep">·</span> {{reports.length}} mode{{#unless (eq reports.length 1)}}s{{/unless}}
189
+ </p>
190
+
191
+ <h2><span class="num">§</span>Summary</h2>
192
+ <table>
193
+ <thead>
194
+ <tr>
195
+ <th>mode</th>
196
+ <th class="num">spins</th>
197
+ <th class="num">measured RTP</th>
198
+ <th class="num">declared</th>
199
+ <th class="num">Δ</th>
200
+ <th class="num">hit rate</th>
201
+ <th>targets</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ {{#each reports}}
206
+ <tr>
207
+ <td class="mono">{{mode.id}}</td>
208
+ <td class="num">{{numLocale spins}}</td>
209
+ <td class="num">{{pct rtp.measured}}</td>
210
+ <td class="num">{{pct rtp.declared}}</td>
211
+ <td class="num {{#if (gt rtp.delta 0)}}delta-pos{{else}}delta-neg{{/if}}">{{pctSigned rtp.delta}}</td>
212
+ <td class="num">{{pct hitRate}}</td>
213
+ <td>{{> targetsBadge}}</td>
214
+ </tr>
215
+ {{/each}}
216
+ </tbody>
217
+ </table>
218
+
219
+ {{#each reports}}
220
+ <h3>
221
+ {{mode.id}}
222
+ {{#if mode.label}}<span class="mode-label">— {{mode.label}}</span>{{/if}}
223
+ </h3>
224
+ <p class="meta">
225
+ math <code>{{math.name}}@{{math.version}}</code> ({{math.kind}})
226
+ <span class="sep">·</span> stake × {{mode.stakeMultiplier}}
227
+ {{#if mode.internal}}<span class="sep">·</span> internal{{/if}}
228
+ <span class="sep">·</span> {{elapsedMs}}ms
229
+ </p>
230
+
231
+ <p class="narrative">{{narrative}}</p>
232
+
233
+ <div class="stat-grid">
234
+ <div class="stat">
235
+ <span class="label">Measured RTP</span>
236
+ <span class="value">{{pct rtp.measured}}</span>
237
+ </div>
238
+ <div class="stat">
239
+ <span class="label">Declared RTP</span>
240
+ <span class="value muted">{{pct rtp.declared}}</span>
241
+ </div>
242
+ <div class="stat">
243
+ <span class="label">Δ</span>
244
+ <span class="value {{#if (gt rtp.delta 0)}}delta-pos{{else}}delta-neg{{/if}}">{{pctSigned rtp.delta}}</span>
245
+ </div>
246
+ <div class="stat">
247
+ <span class="label">Hit Rate</span>
248
+ <span class="value">{{pct hitRate}}</span>
249
+ </div>
250
+ <div class="stat">
251
+ <span class="label">Spins</span>
252
+ <span class="value muted">{{numLocale spins}}</span>
253
+ </div>
254
+ <div class="stat">
255
+ <span class="label">Bet / Spin</span>
256
+ <span class="value muted">{{bet.unitsPerSpin}}u</span>
257
+ </div>
258
+ <div class="stat">
259
+ <span class="label">Max Multiplier</span>
260
+ <span class="value muted">{{fixed win.maxMultiplier 2}}×</span>
261
+ </div>
262
+ <div class="stat">
263
+ <span class="label">Stake ×</span>
264
+ <span class="value muted">{{mode.stakeMultiplier}}</span>
265
+ </div>
266
+ </div>
267
+
268
+ {{#if deviations.length}}
269
+ <h2><span class="num">§</span>Targets vs measured</h2>
270
+ <table>
271
+ <thead>
272
+ <tr>
273
+ <th>metric</th>
274
+ <th class="num">target</th>
275
+ <th class="num">measured</th>
276
+ <th class="num">Δ</th>
277
+ <th class="num">tolerance</th>
278
+ <th>status</th>
279
+ </tr>
280
+ </thead>
281
+ <tbody>
282
+ {{#each deviations}}
283
+ <tr>
284
+ <td class="mono">{{key}}</td>
285
+ <td class="num">{{fixed target 4}}</td>
286
+ <td class="num">{{fixed measured 4}}</td>
287
+ <td class="num {{#if (gt delta 0)}}delta-pos{{else}}delta-neg{{/if}}">{{fixedSigned delta 4}}</td>
288
+ <td class="num">±{{fixed tolerance 4}}</td>
289
+ <td><span class="status status-{{status}}">{{status}}</span></td>
290
+ </tr>
291
+ {{/each}}
292
+ </tbody>
293
+ </table>
294
+ {{/if}}
295
+
296
+ <h2><span class="num">§</span>Multiplier distribution</h2>
297
+ <table>
298
+ <thead><tr><th>stat</th><th class="num">value</th></tr></thead>
299
+ <tbody>
300
+ <tr><td>min</td><td class="num">{{fixed multiplier.min 4}}</td></tr>
301
+ <tr><td>mean</td><td class="num">{{fixed multiplier.mean 4}}</td></tr>
302
+ <tr><td>stddev</td><td class="num">{{fixed multiplier.stdDev 4}}</td></tr>
303
+ <tr><td>p50</td><td class="num">{{fixed multiplier.p50 4}}</td></tr>
304
+ <tr><td>p90</td><td class="num">{{fixed multiplier.p90 4}}</td></tr>
305
+ <tr><td>p95</td><td class="num">{{fixed multiplier.p95 4}}</td></tr>
306
+ <tr><td>p99</td><td class="num">{{fixed multiplier.p99 4}}</td></tr>
307
+ <tr><td>max</td><td class="num">{{fixed multiplier.max 4}}</td></tr>
308
+ </tbody>
309
+ </table>
310
+
311
+ {{#if (anyEntries outcomeTypes)}}
312
+ <h2><span class="num">§</span>Outcome types</h2>
313
+ <table>
314
+ <thead><tr><th>type</th><th class="num">count</th><th class="num">share</th></tr></thead>
315
+ <tbody>
316
+ {{#each (sortEntries outcomeTypes)}}
317
+ <tr><td class="mono">{{key}}</td><td class="num">{{numLocale value}}</td><td class="num">{{pct (div value ../spins)}}</td></tr>
318
+ {{/each}}
319
+ </tbody>
320
+ </table>
321
+ {{/if}}
322
+
323
+ {{#if (anyEntries nextModeRoutes)}}
324
+ <h2><span class="num">§</span>Next-mode routes</h2>
325
+ <table>
326
+ <thead><tr><th>target mode</th><th class="num">count</th><th class="num">share</th></tr></thead>
327
+ <tbody>
328
+ {{#each (sortEntries nextModeRoutes)}}
329
+ <tr><td class="mono">{{key}}</td><td class="num">{{numLocale value}}</td><td class="num">{{pct (div value ../spins)}}</td></tr>
330
+ {{/each}}
331
+ </tbody>
332
+ </table>
333
+ {{/if}}
334
+
335
+ {{#if (anyEntries counters)}}
336
+ <h2><span class="num">§</span>Counters <code>host.mark.count</code></h2>
337
+ <table>
338
+ <thead><tr><th>name</th><th class="num">total</th><th class="num">per spin</th></tr></thead>
339
+ <tbody>
340
+ {{#each (sortCounters counters)}}
341
+ <tr><td class="mono">{{key}}</td><td class="num">{{numLocale value.total}}</td><td class="num">{{fixed value.perSpin 5}}</td></tr>
342
+ {{/each}}
343
+ </tbody>
344
+ </table>
345
+ {{/if}}
346
+
347
+ {{#if (anyEntries observations)}}
348
+ <h2><span class="num">§</span>Observations <code>host.mark.observe</code></h2>
349
+ <table>
350
+ <thead><tr>
351
+ <th>name</th>
352
+ <th class="num">count</th>
353
+ <th class="num">mean</th>
354
+ <th class="num">stddev</th>
355
+ <th class="num">p50</th>
356
+ <th class="num">p90</th>
357
+ <th class="num">p99</th>
358
+ <th class="num">max</th>
359
+ </tr></thead>
360
+ <tbody>
361
+ {{#each (entries observations)}}
362
+ <tr>
363
+ <td class="mono">{{key}}</td>
364
+ <td class="num">{{numLocale value.count}}</td>
365
+ <td class="num">{{fixed value.mean 4}}</td>
366
+ <td class="num">{{fixed value.stdDev 4}}</td>
367
+ <td class="num">{{fixed value.p50 4}}</td>
368
+ <td class="num">{{fixed value.p90 4}}</td>
369
+ <td class="num">{{fixed value.p99 4}}</td>
370
+ <td class="num">{{fixed value.max 4}}</td>
371
+ </tr>
372
+ {{/each}}
373
+ </tbody>
374
+ </table>
375
+ {{/if}}
376
+
377
+ {{#if (anyEntries tagShares)}}
378
+ <h2><span class="num">§</span>Tag shares <code>host.mark.tag</code></h2>
379
+ <table>
380
+ <thead><tr><th>tag</th><th class="num">spins</th><th class="num">share</th></tr></thead>
381
+ <tbody>
382
+ {{#each (sortTags tagShares)}}
383
+ <tr><td class="mono">{{key}}</td><td class="num">{{numLocale value.spins}}</td><td class="num">{{pct value.share}}</td></tr>
384
+ {{/each}}
385
+ </tbody>
386
+ </table>
387
+ {{/if}}
388
+
389
+ {{#if (anyEntries rtpContributions)}}
390
+ <h2><span class="num">§</span>RTP contributions <code>host.mark.contribute</code></h2>
391
+ <table>
392
+ <thead><tr><th>bucket</th><th class="num">sum multiplier</th><th class="num">RTP share</th></tr></thead>
393
+ <tbody>
394
+ {{#each (sortContributions rtpContributions)}}
395
+ <tr><td class="mono">{{key}}</td><td class="num">{{fixed value.sumMultiplier 2}}</td><td class="num">{{pct value.rtpShare}}</td></tr>
396
+ {{/each}}
397
+ </tbody>
398
+ </table>
399
+ {{/if}}
400
+
401
+ {{#if complex}}
402
+ <h2><span class="num">§</span>Complex-round stats</h2>
403
+ <p>Average steps per round: <strong>{{fixed complex.averageStepsPerRound 2}}</strong></p>
404
+ {{/if}}
405
+
406
+ {{/each}}
407
+
408
+ <div class="footer">
409
+ <span>{{generator}}</span>
410
+ <span>{{generatedAt}}</span>
411
+ </div>
412
+ </main>
413
+
414
+ <script>
415
+ (function () {
416
+ var btn = document.getElementById("theme-toggle");
417
+ if (!btn) return;
418
+ btn.addEventListener("click", function () {
419
+ var cur = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
420
+ var next = cur === "dark" ? "light" : "dark";
421
+ document.documentElement.dataset.theme = next;
422
+ try { localStorage.setItem("orgs-sim-theme", next); } catch (e) {}
423
+ });
424
+ })();
425
+ </script>
426
+
427
+ </body>
428
+ </html>
429
+
430
+ {{!-- Handlebars partial: targets summary badge for the summary table --}}
431
+ {{#*inline "targetsBadge"}}
432
+ {{#if deviations.length}}
433
+ {{#each (statusCounts deviations)}}<span class="status status-{{key}}">{{value}} {{key}}</span> {{/each}}
434
+ {{else}}<span class="empty">—</span>{{/if}}
435
+ {{/inline}}