|
- import { RollStats } from "./rollstats"
- import { Outcomes } from "./asphodice"
- import palette from "google-palette"
- import { Chart } from "chart.js"
- import "bootstrap";
-
- // TODO: more descriptive name
- interface ResultPropertyOptions {
- rollstats: RollStats,
- diceClass: string,
- diceVariant: string,
- numDice: number,
- }
-
- /**
- * Provide properties for displaying roll results
- *
- * - rollstats is an instance of a RollStatsClass
- * - diceClass is the shortname of a dice class (eg "aphodice")
- * - diceVariant is a string/number combo to disambiguate (eg "c8" for successCutOff = 8)
- * - 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)
- *
- */
- class ResultProperties {
- rollstats: RollStats;
- diceClass: string;
- diceVariant: string;
- numDice: number;
-
- constructor(rpOptions: ResultPropertyOptions) {
- this.rollstats = rpOptions.rollstats;
- this.diceClass = rpOptions.diceClass;
- this.diceVariant = rpOptions.diceVariant;
- this.numDice = rpOptions.numDice;
- }
-
- /**
- * Provide unique itemId for use in DOM elements, eg "asphodice-c6-d4"
- * for use in, say: <canvas id="balanceCounts-asphodice-c6-d4"></canvas>
- */
- itemId(): string {
- return `${this.diceClass}-${this.diceVariant}-${this.numDice}`;
- }
-
- /**
- * Provide 'variant class', intended use is for result card visibility group toggling
- */
- variantClass(): string {
- return `${this.diceClass}-${this.diceVariant}`;
- }
- }
-
- let barChartOptions = { scales: {
- yAxes: [{
- ticks: {
- beginAtZero: true,
- }
- }]
- },
- legend: {
- display: false,
- }
- }
-
- function hexColours(numColours: number): Array<string> {
- return palette(["cb-Set1", "tol-dv"], numColours).map( function (hex: string) {
- return `#${String(hex)}`; })
- }
- /**
- * Make a table with headings
- *
- * TODO: boolean for striped
- */
- function makeTableSkeleton(tableId: string, headings: Array<string>) {
- let output = `<table id="${tableId}" class="table table-striped">
- <thead>`;
- for (let head of headings) {
- output += `<th>${head}</th>`;
- }
- output += "</thead><tbody></tbody></table>";
- return output;
- }
-
- /**
- * Remap true to "rerolled" and false to "Not rerolled"
- */
- function mapRerolledKeys(Keys: Array<string>): Array<string> {
- return Keys.map((k) => {return (k == "true") ? "Rerolled" : "Not Rerolled"});
- }
-
- function buildData(): string {
- let output = "";
- return output;
- }
-
- function buildCharts(): string {
- let output = "";
- return output;
- }
-
- /**
- * h2 wrapper
- */
- function bigTitle(text:string): string {
- return `<h2 class="card-title">${text}</h2>`;
- }
-
- /**
- * h3 wrapper
- */
- function subTitle(text:string): string {
- return `<h3 class="card-subtitle my-1">${text}</h3>`;
- }
-
- /**
- * h4 wrapper
- */
- function subsubTitle(text:string): string {
- return `<h4 class="card-subtitle my-1">${text}</h4>`;
- }
-
- /**
- * Report on rerolls
- * ie:
- * - make chart
- * - make number boxes
- */
-
- function rerollReport(resultProperties: ResultProperties): string {
- // TODO: this mixes structure and content, probably a good idea to separate it out
- let rerollsChartId = `rerollsChart-${resultProperties.itemId()}`;
- // Column structure
- let output = `<div class="col" id="rerollCol">`;
- // Heading
- output += subTitle("Rerolls");
- // Preamble (ie it looks super confusing)
- //output += "<p>This is super confusing because the colours are swapped on the legend and the chart, then the data below swaps back.</p>";
- // Chart
- output += `<canvas id="${rerollsChartId}"></canvas>`
-
- // Numbers
-
- output += `<div class="row my-3 mx-auto">`;
- output += `<div class="col alert alert-success">
- <strong>Rerolled:</strong>
- ${resultProperties.rollstats.rerollCounts.true}
- (${(resultProperties.rollstats.rerollCounts.true / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
- </div>`;
- output += `<div class="col alert alert-warning">
- <strong>Not rerolled:</strong>
- ${resultProperties.rollstats.rerollCounts.false}
- (${(resultProperties.rollstats.rerollCounts.false / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
- </div>`
- output += `</div>`
- output += `</div>`
- return output;
- }
-
- function outcomesReport(resultProperties: ResultProperties): string {
- let outcomesChartId = `outcomesChart-${resultProperties.itemId()}`;
- let outcomesTableId = `outcomesTable-${resultProperties.itemId()}`;
- // Column structure
- let output = `<div class="col" id="outcomesCol">`;
- // Heading
- output += subTitle("Outcomes");
- // Preamble
-
- // Chart
- output += `<canvas id="${outcomesChartId}"></canvas>`;
-
- // Table
- output += makeTableSkeleton(outcomesTableId, ["Outcome", "Count", "Percentage"]);
-
- output += `</div>`;
- return output;
- }
-
- function outcomeBalancesReport(resultProperties: ResultProperties): string {
- let balanceChartId = `balanceChart-${resultProperties.itemId()}`;
- let balanceTableId = `balanceTable-${resultProperties.itemId()}`;
- // Column structure
- let output = `<div class="col" id="balancesCol">`;
- // Heading
- output += subTitle("Outcome Balances");
- // Preamble
-
- // Chart
- output += `<canvas id="${balanceChartId}"></canvas>`;
-
- // Table
- output += makeTableSkeleton(balanceTableId, ["Balance", "Count", "Percentage"]);
- output += `</div>`;
- return output;
- }
-
- /**
- * Do the charts after we've added them to the canvas
- *
- * The more javascripty way of doing this would be a callback or a custom event (TODO?)
- */
- function generateCharts(resultProperties: ResultProperties): void {
- let itemId = resultProperties.itemId();
- let rerollsChartId = `rerollsChart-${itemId}`;
- let outcomesChartId = `outcomesChart-${itemId}`;
- let balanceChartId = `balanceChart-${itemId}`;
-
- /*
- * Rerolls
- */
-
- let rerollsCanvas: any = $(`#${rerollsChartId}`);
- let rerollsChart = new Chart(rerollsCanvas, {
- type: "doughnut",
- data: {
- labels: mapRerolledKeys(Object.keys(resultProperties.rollstats.rerollCounts)),
- datasets: [{
- label: "Reroll Counts",
- data: Object.values(resultProperties.rollstats.rerollCounts),
- backgroundColor: ["#d1e7dd", "#fff3cd"],
- //backgroundColor: hexColours(Object.values(rollstats.balanceCounts).length),
- }],
- },
- options: {
- title: {
- text: "Rerolls Chart",
- display: false,
- },
- legend: {
- reverse: true,
- }
- }
- });
-
- /*
- * Outcomes
- */
-
- // sort so we have failure before success
- let oc = resultProperties.rollstats.outcomeCounts;
- let okeys = Object.keys(oc).sort();
- let ovalues = [];
- for (let i = 0; i < okeys.length; i++) {
- ovalues.push(oc[okeys[i] as Outcomes]);
- }
-
- let outcomesCanvas: any = $(`#${outcomesChartId}`);
- let outcomesChart = new Chart(outcomesCanvas, {
- type: "bar",
- data: {
- labels: okeys,
- datasets: [{
- label: "Outcome Counts",
- data: ovalues,
- backgroundColor: hexColours(Object.values(resultProperties.rollstats.outcomeCounts).length),
- }],
- },
- options: barChartOptions,
- });
-
- /**
- * Outcome Balances
- */
-
- let bc = resultProperties.rollstats.balanceCounts;
- let keys = Object.keys(bc);
- keys.sort(function(a: string, b: string){return Number(a) - Number(b)});
- // sort values too
- let values = [];
- for (let i = 0; i < keys.length; i++) {
- values.push(bc[keys[i]]);
- }
-
- let balanceCanvas: any = $(`#${balanceChartId}`);
- let balanceChart = new Chart(balanceCanvas, {
- type: "bar",
- data: {
- labels: keys,
- datasets: [{
- label: "Outcome Balance Counts",
- data: values,
- backgroundColor: hexColours(Object.values(resultProperties.rollstats.balanceCounts).length),
- }],
- },
- options: barChartOptions,
- });
-
- }
-
- /**
- * Generate tables after we've inserted them into the DOM,
- *
- * Also should be callback/event-driven. (TODO?)
- */
- function generateTables(resultProperties: ResultProperties): void {
- let itemId = resultProperties.itemId();
- /*
- * Outcomes
- */
- let outcomesTableId = `outcomesTable-${itemId}`;
-
- let oc = resultProperties.rollstats.outcomeCounts;
- let okeys = Object.keys(oc).sort();
- for (var i = 0; i < okeys.length; i++) {
- let tb = $(`#${outcomesTableId}`).find("tbody");
- let outcome: Outcomes = okeys[i] as Outcomes;
- let outcomeCount = resultProperties.rollstats.outcomeCounts[outcome];
- let outcomePercent: string = "";
- if (outcomeCount) {
- // Needed to avoid TS2532:
- outcomePercent = (outcomeCount / resultProperties.rollstats.numRolls * 100).toFixed(2);
- }
- tb.append(`<tr> <td>${outcome}</td>
- <td>${outcomeCount}</td>
- <td>${outcomePercent}</td>
- </tr>`);
- }
-
- /*
- * Outcome Balance Counts
- */
- let balanceTableId = `balanceTable-${itemId}`;
-
- let bc = resultProperties.rollstats.balanceCounts;
- let keys = Object.keys(bc);
- keys.sort(function(a: string, b: string){return Number(a) - Number(b)});
- for (var i = 0; i < keys.length; i++) {
- let tb = $(`#${balanceTableId}`).find("tbody");
- tb.append(`<tr> <td>${keys[i]}</td>
- <td>${resultProperties.rollstats.balanceCounts[keys[i]]}</td>
- <td>${(resultProperties.rollstats.balanceCounts[keys[i]] / resultProperties.rollstats.numRolls * 100).toFixed(2)}</td>
- </tr>`);
- }
-
- }
-
- /**
- * For pretty-printing. See https://stackoverflow.com/a/2901298
- */
- function numberWithCommas(x:number): string {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
- }
-
- /**
- * Give some info on the dice used to generate these results
- */
-
- function describeRolls(resultsProperties: ResultProperties): string {
- let output = subTitle("Description");
- output += `<p>There were <strong>${resultsProperties.numDice}</strong> Asphodice rolled <strong>${numberWithCommas(resultsProperties.rollstats.numRolls)}</strong> times.</p>`;
- return output;
- }
-
- /**
- * Set up results card that contains one set of RollStats results
- */
- function resultsCard(resultsProperties: ResultProperties): JQuery<HTMLElement>{
- // <div id="results"> is the general results div to append results to
- // TODO: include class shortname in id (?)
- let cardId = `results-${resultsProperties.itemId()}`;
-
- $("#results").append(`<div class="card my-4" id="${cardId}">`);
- let resultsCard = $("#results").find(`#${cardId}`);
- resultsCard.append(`<div class="card-body">`);
- let resultsCardBody: JQuery<HTMLElement> = $(`#${cardId}`).find("div.card-body");
- return resultsCardBody;
- }
-
- /**
- * Set up results 'header' - class name (TODO) and number of dice rolled
- */
-
- function resultsHeader(resultProperties: ResultProperties): string {
- // Use flexbox (d-flex) for LHS/RHS justification
- let resultsBodyId = `resultsBody-${resultProperties.itemId()}`;
- let resultsTitle = `<div class="d-flex justify-content-between">`
- resultsTitle += `<div>`
- + bigTitle(`Asphodice Results
- <small class="h4 text-muted">
- (<var>d=${resultProperties.numDice}</var>,
- <var>${resultProperties.diceVariant}</var>)
- </small>`)
- + `</div>`;
- resultsTitle += `<div>
- <button type="button" class="btn btn-primary btn-sm" data-bs-toggle="collapse"
- data-bs-target="#${resultsBodyId}"
- aria-expanded="true" aria-controls="collapse-resultsBody">
- (hide/show)
- </button>
- </div>`
- resultsTitle += `</div>`;
- return resultsTitle;
- }
-
- /**
- * Write out results 'body'. Set up in columns, the 'skeletons' of charts and tables are set up first,
- * to be filled in afterwards
- */
-
- function resultsBody(resultProperties: ResultProperties): string {
- let resultsBodyId = `resultsBody-${resultProperties.itemId()}`;
- let resultsBody = `<div id="${resultsBodyId}" class="show row resultsToggle ${resultProperties.variantClass()}">`;
- resultsBody += describeRolls(resultProperties);
- resultsBody += rerollReport(resultProperties)
- + outcomesReport(resultProperties)
- + outcomeBalancesReport(resultProperties);
- resultsBody += `</div>`;
- return resultsBody;
- }
-
- /**
- * Put the results together - card, head, body → fill charts and tables
- */
- function addResults(resultProperties: ResultProperties): void {
- let resultsCardBody = resultsCard(resultProperties);
-
- resultsCardBody.append(resultsHeader(resultProperties));
-
- resultsCardBody.append(resultsBody(resultProperties));
-
- // Post-DOM-Construction generation
- //
- // For tables: we need this as the way that tables were implemented
- // we .find() the <tbody>
- //
- // For charts: the Chart() generation function expects to passed an extant
- // <canvas> element
-
- // Tables
- generateTables(resultProperties);
-
- // Charts
- generateCharts(resultProperties);
- }
-
- /**
- * Card with status and hide-show all control
- */
- function resultsControlCard(): string {
- let resultsControl = `<div id="resultsControl" class="card my-3 mx-2 border border-2 border-info">`;
- resultsControl += `<h2 class="card-title mx-3 my-2">Results</h5>`;
-
- resultsControl += `<div class="progress mx-3">
- <div class="progress-bar" id="resultsProgress"></div>
- </div>`;
-
- resultsControl += `<div class="card-body">
- <button class="btn btn-primary" type="button"
- data-bs-toggle="collapse" data-bs-target=".resultsToggle"
- aria-expanded="true" aria-controls="resultsToggleAll">
- Hide / Show All
- </button>
- </div>`;
-
- resultsControl += `</div>`;
- return resultsControl
- }
-
- /**
- * Add control to show/hide all of particular variant to control Card
- */
- function addVariantControl(controlCard: JQuery<HTMLElement>,
- resultProperties: ResultProperties): void {
- let variantClass = resultProperties.variantClass();
- controlCard.append(`<button id="variantButton-${variantClass}"
- class="btn btn-primary" type="button"
- data-bs-toggle="collapse" data-bs-target=".${variantClass}"
- aria-expanded="true" aria-controls="resultsToggleVariant">
- Hide / Show Variant (${resultProperties.diceVariant})
- </button>
- `);
- }
-
- function getResults():void {
- // Disable 'roll' button
- $("#mainRoll").prop("disabled", true);
- console.log("Getting results...");
-
- $("#results").empty();
-
- $("#results").append(resultsControlCard);
- let controlCardBody = $("#results").find("#resultsControl .card-body");
-
-
- let maxDice = 10;
- let cutoffStart = 6;
- let cutoffMax = 9;
- for (let cutoff = cutoffStart; cutoff <= cutoffMax; cutoff++) {
- let variantControlAdded = false;
- for (let i = 1; i < maxDice; i++) {
- let rsSetup = { numDice: i, diceOptions: { successCutOff: cutoff } };
- let rollstats = new RollStats(rsSetup);
- let resultProperties = new ResultProperties({
- rollstats: rollstats,
- diceClass: "asphodice",
- diceVariant: `c${cutoff}`,
- numDice: i,
- });
- rollstats.doRolls();
- addResults(resultProperties);
- // Add control for variant if it doesn't exist
- if (!variantControlAdded) {
- addVariantControl(controlCardBody, resultProperties);
- variantControlAdded = true;
- }
- $("#resultsProgress").width(`${i/maxDice*100}%`);
- $("#resultsProgress").text(`${i/maxDice*100}%`);
- }
- }
- console.log("Results done!");
- $("#resultsProgress").width("100%");
- $("#resultsProgress").text("100%");
- }
-
-
- function setupHandlers(): void {
- $("#mainRoll").click(getResults);
- }
-
- document.addEventListener("DOMContentLoaded", setupHandlers);
|