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: */ 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 { 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) { let output = ``; for (let head of headings) { output += ``; } output += "
${head}
"; return output; } /** * Remap true to "rerolled" and false to "Not rerolled" */ function mapRerolledKeys(Keys: Array): Array { 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 `

${text}

`; } /** * h3 wrapper */ function subTitle(text:string): string { return `

${text}

`; } /** * h4 wrapper */ function subsubTitle(text:string): string { return `

${text}

`; } /** * 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 = `
`; // Heading output += subTitle("Rerolls"); // Preamble (ie it looks super confusing) //output += "

This is super confusing because the colours are swapped on the legend and the chart, then the data below swaps back.

"; // Chart output += `` // Numbers output += `
`; output += `
Rerolled: ${resultProperties.rollstats.rerollCounts.true} (${(resultProperties.rollstats.rerollCounts.true / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
`; output += `
Not rerolled: ${resultProperties.rollstats.rerollCounts.false} (${(resultProperties.rollstats.rerollCounts.false / resultProperties.rollstats.numRolls * 100).toFixed(2)} %)
` output += `
` output += `
` return output; } function outcomesReport(resultProperties: ResultProperties): string { let outcomesChartId = `outcomesChart-${resultProperties.itemId()}`; let outcomesTableId = `outcomesTable-${resultProperties.itemId()}`; // Column structure let output = `
`; // Heading output += subTitle("Outcomes"); // Preamble // Chart output += ``; // Table output += makeTableSkeleton(outcomesTableId, ["Outcome", "Count", "Percentage"]); output += `
`; return output; } function outcomeBalancesReport(resultProperties: ResultProperties): string { let balanceChartId = `balanceChart-${resultProperties.itemId()}`; let balanceTableId = `balanceTable-${resultProperties.itemId()}`; // Column structure let output = `
`; // Heading output += subTitle("Outcome Balances"); // Preamble // Chart output += ``; // Table output += makeTableSkeleton(balanceTableId, ["Balance", "Count", "Percentage"]); output += `
`; 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(` ${outcome} ${outcomeCount} ${outcomePercent} `); } /* * 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(` ${keys[i]} ${resultProperties.rollstats.balanceCounts[keys[i]]} ${(resultProperties.rollstats.balanceCounts[keys[i]] / resultProperties.rollstats.numRolls * 100).toFixed(2)} `); } } /** * 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 += `

There were ${resultsProperties.numDice} Asphodice rolled ${numberWithCommas(resultsProperties.rollstats.numRolls)} times.

`; return output; } /** * Set up results card that contains one set of RollStats results */ function resultsCard(resultsProperties: ResultProperties): JQuery{ //
is the general results div to append results to // TODO: include class shortname in id (?) let cardId = `results-${resultsProperties.itemId()}`; $("#results").append(`
`); let resultsCard = $("#results").find(`#${cardId}`); resultsCard.append(`
`); let resultsCardBody: JQuery = $(`#${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 = `
` resultsTitle += `
` + bigTitle(`Asphodice Results (d=${resultProperties.numDice}, ${resultProperties.diceVariant}) `) + `
`; resultsTitle += `
` resultsTitle += `
`; 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 = `
`; resultsBody += describeRolls(resultProperties); resultsBody += rerollReport(resultProperties) + outcomesReport(resultProperties) + outcomeBalancesReport(resultProperties); resultsBody += `
`; 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 // // For charts: the Chart() generation function expects to passed an extant // element // Tables generateTables(resultProperties); // Charts generateCharts(resultProperties); } /** * Card with status and hide-show all control */ function resultsControlCard(): string { let resultsControl = `
`; resultsControl += `

Results

`; resultsControl += `
`; resultsControl += `
`; resultsControl += `
`; return resultsControl } /** * Add control to show/hide all of particular variant to control Card */ function addVariantControl(controlCard: JQuery, resultProperties: ResultProperties): void { let variantClass = resultProperties.variantClass(); controlCard.append(` `); } 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);