Roll dice (eg Asphodice) and show outcomes https://rpg.bertieb.org/dice-roller/
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

520 satır
15 KiB

  1. import { RollStats } from "./rollstats"
  2. import { Outcomes } from "./asphodice"
  3. import palette from "google-palette"
  4. import { Chart } from "chart.js"
  5. import "bootstrap";
  6. // TODO: more descriptive name
  7. interface ResultPropertyOptions {
  8. rollstats: RollStats,
  9. diceClass: string,
  10. diceVariant: string,
  11. numDice: number,
  12. }
  13. /**
  14. * Provide properties for displaying roll results
  15. *
  16. * - rollstats is an instance of a RollStatsClass
  17. * - diceClass is the shortname of a dice class (eg "aphodice")
  18. * - diceVariant is a string/number combo to disambiguate (eg "c8" for successCutOff = 8)
  19. * - numDice is the number of dice being rolled at a particular time (eg 4; NB negative numbers indicate randomised dice number though not used at present)
  20. *
  21. */
  22. class ResultProperties {
  23. rollstats: RollStats;
  24. diceClass: string;
  25. diceVariant: string;
  26. numDice: number;
  27. constructor(rpOptions: ResultPropertyOptions) {
  28. this.rollstats = rpOptions.rollstats;
  29. this.diceClass = rpOptions.diceClass;
  30. this.diceVariant = rpOptions.diceVariant;
  31. this.numDice = rpOptions.numDice;
  32. }
  33. /**
  34. * Provide unique itemId for use in DOM elements, eg "asphodice-c6-d4"
  35. * for use in, say: <canvas id="balanceCounts-asphodice-c6-d4"></canvas>
  36. */
  37. itemId(): string {
  38. return `${this.diceClass}-${this.diceVariant}-${this.numDice}`;
  39. }
  40. /**
  41. * Provide 'variant class', intended use is for result card visibility group toggling
  42. */
  43. variantClass(): string {
  44. return `${this.diceClass}-${this.diceVariant}`;
  45. }
  46. }
  47. let barChartOptions = { scales: {
  48. yAxes: [{
  49. ticks: {
  50. beginAtZero: true,
  51. }
  52. }]
  53. },
  54. legend: {
  55. display: false,
  56. }
  57. }
  58. function hexColours(numColours: number): Array<string> {
  59. return palette(["cb-Set1", "tol-dv"], numColours).map( function (hex: string) {
  60. return `#${String(hex)}`; })
  61. }
  62. /**
  63. * Make a table with headings
  64. *
  65. * TODO: boolean for striped
  66. */
  67. function makeTableSkeleton(tableId: string, headings: Array<string>) {
  68. let output = `<table id="${tableId}" class="table table-striped">
  69. <thead>`;
  70. for (let head of headings) {
  71. output += `<th>${head}</th>`;
  72. }
  73. output += "</thead><tbody></tbody></table>";
  74. return output;
  75. }
  76. /**
  77. * Remap true to "rerolled" and false to "Not rerolled"
  78. */
  79. function mapRerolledKeys(Keys: Array<string>): Array<string> {
  80. return Keys.map((k) => {return (k == "true") ? "Rerolled" : "Not Rerolled"});
  81. }
  82. function buildData(): string {
  83. let output = "";
  84. return output;
  85. }
  86. function buildCharts(): string {
  87. let output = "";
  88. return output;
  89. }
  90. /**
  91. * h2 wrapper
  92. */
  93. function bigTitle(text:string): string {
  94. return `<h2 class="card-title">${text}</h2>`;
  95. }
  96. /**
  97. * h3 wrapper
  98. */
  99. function subTitle(text:string): string {
  100. return `<h3 class="card-subtitle my-1">${text}</h3>`;
  101. }
  102. /**
  103. * h4 wrapper
  104. */
  105. function subsubTitle(text:string): string {
  106. return `<h4 class="card-subtitle my-1">${text}</h4>`;
  107. }
  108. /**
  109. * Report on rerolls
  110. * ie:
  111. * - make chart
  112. * - make number boxes
  113. */
  114. function rerollReport(resultProperties: ResultProperties): string {
  115. // TODO: this mixes structure and content, probably a good idea to separate it out
  116. let rerollsChartId = `rerollsChart-${resultProperties.itemId()}`;
  117. // Column structure
  118. let output = `<div class="col" id="rerollCol">`;
  119. // Heading
  120. output += subTitle("Rerolls");
  121. // Preamble (ie it looks super confusing)
  122. //output += "<p>This is super confusing because the colours are swapped on the legend and the chart, then the data below swaps back.</p>";
  123. // Chart
  124. output += `<canvas id="${rerollsChartId}"></canvas>`
  125. // Numbers
  126. output += `<div class="row my-3 mx-auto">`;
  127. output += `<div class="col alert alert-success">
  128. <strong>Rerolled:</strong>
  129. ${resultProperties.rollstats.rerollCounts.true}
  130. (${(resultProperties.rollstats.rerollCounts.true / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
  131. </div>`;
  132. output += `<div class="col alert alert-warning">
  133. <strong>Not rerolled:</strong>
  134. ${resultProperties.rollstats.rerollCounts.false}
  135. (${(resultProperties.rollstats.rerollCounts.false / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
  136. </div>`
  137. output += `</div>`
  138. output += `</div>`
  139. return output;
  140. }
  141. function outcomesReport(resultProperties: ResultProperties): string {
  142. let outcomesChartId = `outcomesChart-${resultProperties.itemId()}`;
  143. let outcomesTableId = `outcomesTable-${resultProperties.itemId()}`;
  144. // Column structure
  145. let output = `<div class="col" id="outcomesCol">`;
  146. // Heading
  147. output += subTitle("Outcomes");
  148. // Preamble
  149. // Chart
  150. output += `<canvas id="${outcomesChartId}"></canvas>`;
  151. // Table
  152. output += makeTableSkeleton(outcomesTableId, ["Outcome", "Count", "Percentage"]);
  153. output += `</div>`;
  154. return output;
  155. }
  156. function outcomeBalancesReport(resultProperties: ResultProperties): string {
  157. let balanceChartId = `balanceChart-${resultProperties.itemId()}`;
  158. let balanceTableId = `balanceTable-${resultProperties.itemId()}`;
  159. // Column structure
  160. let output = `<div class="col" id="balancesCol">`;
  161. // Heading
  162. output += subTitle("Outcome Balances");
  163. // Preamble
  164. // Chart
  165. output += `<canvas id="${balanceChartId}"></canvas>`;
  166. // Table
  167. output += makeTableSkeleton(balanceTableId, ["Balance", "Count", "Percentage"]);
  168. output += `</div>`;
  169. return output;
  170. }
  171. /**
  172. * Do the charts after we've added them to the canvas
  173. *
  174. * The more javascripty way of doing this would be a callback or a custom event (TODO?)
  175. */
  176. function generateCharts(resultProperties: ResultProperties): void {
  177. let itemId = resultProperties.itemId();
  178. let rerollsChartId = `rerollsChart-${itemId}`;
  179. let outcomesChartId = `outcomesChart-${itemId}`;
  180. let balanceChartId = `balanceChart-${itemId}`;
  181. /*
  182. * Rerolls
  183. */
  184. let rerollsCanvas: any = $(`#${rerollsChartId}`);
  185. let rerollsChart = new Chart(rerollsCanvas, {
  186. type: "doughnut",
  187. data: {
  188. labels: mapRerolledKeys(Object.keys(resultProperties.rollstats.rerollCounts)),
  189. datasets: [{
  190. label: "Reroll Counts",
  191. data: Object.values(resultProperties.rollstats.rerollCounts),
  192. backgroundColor: ["#d1e7dd", "#fff3cd"],
  193. //backgroundColor: hexColours(Object.values(rollstats.balanceCounts).length),
  194. }],
  195. },
  196. options: {
  197. title: {
  198. text: "Rerolls Chart",
  199. display: false,
  200. },
  201. legend: {
  202. reverse: true,
  203. }
  204. }
  205. });
  206. /*
  207. * Outcomes
  208. */
  209. // sort so we have failure before success
  210. let oc = resultProperties.rollstats.outcomeCounts;
  211. let okeys = Object.keys(oc).sort();
  212. let ovalues = [];
  213. for (let i = 0; i < okeys.length; i++) {
  214. ovalues.push(oc[okeys[i] as Outcomes]);
  215. }
  216. let outcomesCanvas: any = $(`#${outcomesChartId}`);
  217. let outcomesChart = new Chart(outcomesCanvas, {
  218. type: "bar",
  219. data: {
  220. labels: okeys,
  221. datasets: [{
  222. label: "Outcome Counts",
  223. data: ovalues,
  224. backgroundColor: hexColours(Object.values(resultProperties.rollstats.outcomeCounts).length),
  225. }],
  226. },
  227. options: barChartOptions,
  228. });
  229. /**
  230. * Outcome Balances
  231. */
  232. let bc = resultProperties.rollstats.balanceCounts;
  233. let keys = Object.keys(bc);
  234. keys.sort(function(a: string, b: string){return Number(a) - Number(b)});
  235. // sort values too
  236. let values = [];
  237. for (let i = 0; i < keys.length; i++) {
  238. values.push(bc[keys[i]]);
  239. }
  240. let balanceCanvas: any = $(`#${balanceChartId}`);
  241. let balanceChart = new Chart(balanceCanvas, {
  242. type: "bar",
  243. data: {
  244. labels: keys,
  245. datasets: [{
  246. label: "Outcome Balance Counts",
  247. data: values,
  248. backgroundColor: hexColours(Object.values(resultProperties.rollstats.balanceCounts).length),
  249. }],
  250. },
  251. options: barChartOptions,
  252. });
  253. }
  254. /**
  255. * Generate tables after we've inserted them into the DOM,
  256. *
  257. * Also should be callback/event-driven. (TODO?)
  258. */
  259. function generateTables(resultProperties: ResultProperties): void {
  260. let itemId = resultProperties.itemId();
  261. /*
  262. * Outcomes
  263. */
  264. let outcomesTableId = `outcomesTable-${itemId}`;
  265. let oc = resultProperties.rollstats.outcomeCounts;
  266. let okeys = Object.keys(oc).sort();
  267. for (var i = 0; i < okeys.length; i++) {
  268. let tb = $(`#${outcomesTableId}`).find("tbody");
  269. let outcome: Outcomes = okeys[i] as Outcomes;
  270. let outcomeCount = resultProperties.rollstats.outcomeCounts[outcome];
  271. let outcomePercent: string = "";
  272. if (outcomeCount) {
  273. // Needed to avoid TS2532:
  274. outcomePercent = (outcomeCount / resultProperties.rollstats.numRolls * 100).toFixed(2);
  275. }
  276. tb.append(`<tr> <td>${outcome}</td>
  277. <td>${outcomeCount}</td>
  278. <td>${outcomePercent}</td>
  279. </tr>`);
  280. }
  281. /*
  282. * Outcome Balance Counts
  283. */
  284. let balanceTableId = `balanceTable-${itemId}`;
  285. let bc = resultProperties.rollstats.balanceCounts;
  286. let keys = Object.keys(bc);
  287. keys.sort(function(a: string, b: string){return Number(a) - Number(b)});
  288. for (var i = 0; i < keys.length; i++) {
  289. let tb = $(`#${balanceTableId}`).find("tbody");
  290. tb.append(`<tr> <td>${keys[i]}</td>
  291. <td>${resultProperties.rollstats.balanceCounts[keys[i]]}</td>
  292. <td>${(resultProperties.rollstats.balanceCounts[keys[i]] / resultProperties.rollstats.numRolls * 100).toFixed(2)}</td>
  293. </tr>`);
  294. }
  295. }
  296. /**
  297. * For pretty-printing. See https://stackoverflow.com/a/2901298
  298. */
  299. function numberWithCommas(x:number): string {
  300. return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  301. }
  302. /**
  303. * Give some info on the dice used to generate these results
  304. */
  305. function describeRolls(resultsProperties: ResultProperties): string {
  306. let output = subTitle("Description");
  307. output += `<p>There were <strong>${resultsProperties.numDice}</strong> Asphodice rolled <strong>${numberWithCommas(resultsProperties.rollstats.numRolls)}</strong> times.</p>`;
  308. return output;
  309. }
  310. /**
  311. * Set up results card that contains one set of RollStats results
  312. */
  313. function resultsCard(resultsProperties: ResultProperties): JQuery<HTMLElement>{
  314. // <div id="results"> is the general results div to append results to
  315. // TODO: include class shortname in id (?)
  316. let cardId = `results-${resultsProperties.itemId()}`;
  317. $("#results").append(`<div class="card my-4" id="${cardId}">`);
  318. let resultsCard = $("#results").find(`#${cardId}`);
  319. resultsCard.append(`<div class="card-body">`);
  320. let resultsCardBody: JQuery<HTMLElement> = $(`#${cardId}`).find("div.card-body");
  321. return resultsCardBody;
  322. }
  323. /**
  324. * Set up results 'header' - class name (TODO) and number of dice rolled
  325. */
  326. function resultsHeader(resultProperties: ResultProperties): string {
  327. // Use flexbox (d-flex) for LHS/RHS justification
  328. let resultsBodyId = `resultsBody-${resultProperties.itemId()}`;
  329. let resultsTitle = `<div class="d-flex justify-content-between">`
  330. resultsTitle += `<div>`
  331. + bigTitle(`Asphodice Results
  332. <small class="h4 text-muted">
  333. (<var>d=${resultProperties.numDice}</var>,
  334. <var>${resultProperties.diceVariant}</var>)
  335. </small>`)
  336. + `</div>`;
  337. resultsTitle += `<div>
  338. <button type="button" class="btn btn-primary btn-sm" data-bs-toggle="collapse"
  339. data-bs-target="#${resultsBodyId}"
  340. aria-expanded="true" aria-controls="collapse-resultsBody">
  341. (hide/show)
  342. </button>
  343. </div>`
  344. resultsTitle += `</div>`;
  345. return resultsTitle;
  346. }
  347. /**
  348. * Write out results 'body'. Set up in columns, the 'skeletons' of charts and tables are set up first,
  349. * to be filled in afterwards
  350. */
  351. function resultsBody(resultProperties: ResultProperties): string {
  352. let resultsBodyId = `resultsBody-${resultProperties.itemId()}`;
  353. let resultsBody = `<div id="${resultsBodyId}" class="show row resultsToggle ${resultProperties.variantClass()}">`;
  354. resultsBody += describeRolls(resultProperties);
  355. resultsBody += rerollReport(resultProperties)
  356. + outcomesReport(resultProperties)
  357. + outcomeBalancesReport(resultProperties);
  358. resultsBody += `</div>`;
  359. return resultsBody;
  360. }
  361. /**
  362. * Put the results together - card, head, body → fill charts and tables
  363. */
  364. function addResults(resultProperties: ResultProperties): void {
  365. let resultsCardBody = resultsCard(resultProperties);
  366. resultsCardBody.append(resultsHeader(resultProperties));
  367. resultsCardBody.append(resultsBody(resultProperties));
  368. // Post-DOM-Construction generation
  369. //
  370. // For tables: we need this as the way that tables were implemented
  371. // we .find() the <tbody>
  372. //
  373. // For charts: the Chart() generation function expects to passed an extant
  374. // <canvas> element
  375. // Tables
  376. generateTables(resultProperties);
  377. // Charts
  378. generateCharts(resultProperties);
  379. }
  380. /**
  381. * Card with status and hide-show all control
  382. */
  383. function resultsControlCard(): string {
  384. let resultsControl = `<div id="resultsControl" class="card my-3 mx-2 border border-2 border-info">`;
  385. resultsControl += `<h2 class="card-title mx-3 my-2">Results</h5>`;
  386. resultsControl += `<div class="progress mx-3">
  387. <div class="progress-bar" id="resultsProgress"></div>
  388. </div>`;
  389. resultsControl += `<div class="card-body">
  390. <button class="btn btn-primary" type="button"
  391. data-bs-toggle="collapse" data-bs-target=".resultsToggle"
  392. aria-expanded="true" aria-controls="resultsToggleAll">
  393. Hide / Show All
  394. </button>
  395. </div>`;
  396. resultsControl += `</div>`;
  397. return resultsControl
  398. }
  399. /**
  400. * Add control to show/hide all of particular variant to control Card
  401. */
  402. function addVariantControl(controlCard: JQuery<HTMLElement>,
  403. resultProperties: ResultProperties): void {
  404. let variantClass = resultProperties.variantClass();
  405. controlCard.append(`<button id="variantButton-${variantClass}"
  406. class="btn btn-primary" type="button"
  407. data-bs-toggle="collapse" data-bs-target=".${variantClass}"
  408. aria-expanded="true" aria-controls="resultsToggleVariant">
  409. Hide / Show Variant (${resultProperties.diceVariant})
  410. </button>
  411. `);
  412. }
  413. function getResults():void {
  414. // Disable 'roll' button
  415. $("#mainRoll").prop("disabled", true);
  416. console.log("Getting results...");
  417. $("#results").empty();
  418. $("#results").append(resultsControlCard);
  419. let controlCardBody = $("#results").find("#resultsControl .card-body");
  420. let maxDice = 10;
  421. let cutoffStart = 6;
  422. let cutoffMax = 9;
  423. for (let cutoff = cutoffStart; cutoff <= cutoffMax; cutoff++) {
  424. let variantControlAdded = false;
  425. for (let i = 1; i < maxDice; i++) {
  426. let rsSetup = { numDice: i, diceOptions: { successCutOff: cutoff } };
  427. let rollstats = new RollStats(rsSetup);
  428. let resultProperties = new ResultProperties({
  429. rollstats: rollstats,
  430. diceClass: "asphodice",
  431. diceVariant: `c${cutoff}`,
  432. numDice: i,
  433. });
  434. rollstats.doRolls();
  435. addResults(resultProperties);
  436. // Add control for variant if it doesn't exist
  437. if (!variantControlAdded) {
  438. addVariantControl(controlCardBody, resultProperties);
  439. variantControlAdded = true;
  440. }
  441. $("#resultsProgress").width(`${i/maxDice*100}%`);
  442. $("#resultsProgress").text(`${i/maxDice*100}%`);
  443. }
  444. }
  445. console.log("Results done!");
  446. $("#resultsProgress").width("100%");
  447. $("#resultsProgress").text("100%");
  448. }
  449. function setupHandlers(): void {
  450. $("#mainRoll").click(getResults);
  451. }
  452. document.addEventListener("DOMContentLoaded", setupHandlers);