UNPKG

modaq

Version:

Quiz Bowl Reader using TypeScript, React, and MobX

282 lines 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScoresheetDialogBody = exports.ScoresheetDialog = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const React = __importStar(require("react")); const mobx_react_lite_1 = require("mobx-react-lite"); const FormattedTextParser = __importStar(require("../../parser/FormattedTextParser")); const PlayerUtils = __importStar(require("../../state/PlayerUtils")); require("../../state/Cycle"); require("../../state/AppState"); const react_1 = require("@fluentui/react"); const StateContext_1 = require("../../contexts/StateContext"); require("../../state/GameState"); require("../../state/TeamState"); require("../../state/Events"); const ModalVisibilityStatus_1 = require("../../state/ModalVisibilityStatus"); const ModalDialog_1 = require("./ModalDialog"); // Based on the scoresheet created by Ryan Rosenberg, like the one here: https://quizbowlstats.com/games/95741 exports.ScoresheetDialog = mobx_react_lite_1.observer(function ScoresheetDialog() { const appState = React.useContext(StateContext_1.StateContext); const closeDialog = React.useCallback(() => appState.uiState.dialogState.hideModalDialog(), [appState]); return (jsx_runtime_1.jsxs(ModalDialog_1.ModalDialog, Object.assign({ title: "Scoresheet", visibilityStatus: ModalVisibilityStatus_1.ModalVisibilityStatus.Scoresheet, maxWidth: "100vw", onDismiss: closeDialog }, { children: [jsx_runtime_1.jsx(exports.ScoresheetDialogBody, { appState: appState }, void 0), jsx_runtime_1.jsx(react_1.DialogFooter, { children: jsx_runtime_1.jsx(react_1.PrimaryButton, { text: "Close", onClick: closeDialog }, void 0) }, void 0)] }), void 0)); }); exports.ScoresheetDialogBody = mobx_react_lite_1.observer(function ScoresheetDialogBody(props) { const appState = props.appState; const game = appState.game; return (jsx_runtime_1.jsx(react_1.ThemeContext.Consumer, { children: (theme) => { const classNames = getClassNames(theme); const totalScoreClassNames = `${classNames.totalScoreCell} ${classNames.tableCell}`; const tuNumberClassNames = `${classNames.tableCell} ${classNames.tuNumber}`; const playerToStatlineMap = new Map(); const teamToPlayerMap = new Map(); const teamToActivePlayerMap = new Map(); for (const teamName of game.teamNames) { teamToPlayerMap.set(teamName, []); teamToActivePlayerMap.set(teamName, game.getActivePlayers(teamName, 0)); } for (const player of game.players) { // game.teamNames will have every team a player is on, so we know the array exists teamToPlayerMap.get(player.teamName).push(player); playerToStatlineMap.set(player, new Map()); } const cyclesRows = []; for (let i = 0; i < game.playableCycles.length; i++) { let isFirstTeamName = true; const cells = []; const cycle = game.playableCycles[i]; for (let j = 0; j < game.teamNames.length; j++) { const teamName = game.teamNames[j]; if (!isFirstTeamName) { // Add TU number const number = i + 1; cells.push(jsx_runtime_1.jsx("td", Object.assign({ className: tuNumberClassNames }, { children: number }), `TU_${number}`)); } else { isFirstTeamName = false; } // We initialized this before, we'll always have the array const teamPlayers = teamToPlayerMap.get(teamName); // getActivePlayers takes O(n) where n is the cycle, so doing this each time in the loop would be // quadratic. It's not too big of a hit but we do gain several ms from caching this // We could make this more efficient in some cases by making sure that the team involved is // in one of these events, but this shouldn't be too big of a hit if (cycle.playerJoins || cycle.playerLeaves || cycle.subs) { teamToActivePlayerMap.set(teamName, game.getActivePlayers(teamName, i)); } // We know this always exists since we set it up before const activeTeamPlayers = teamToActivePlayerMap.get(teamName); for (const player of teamPlayers) { cells.push(renderPlayerCell(game, player, cycle, activeTeamPlayers, playerToStatlineMap, i, classNames)); } cells.push(renderBonusCell(cycle, teamName, i, classNames)); cells.push(jsx_runtime_1.jsx("td", Object.assign({ className: totalScoreClassNames }, { children: game.scores[i][j] }), `Total_${i}_${j}`)); } cyclesRows.push(jsx_runtime_1.jsx("tr", Object.assign({ className: classNames.cycleRow }, { children: cells }), `Row_${i}`)); } cyclesRows.push(renderStatlineRow(game, teamToPlayerMap, playerToStatlineMap, classNames)); const teamTitle = game.teamNames.join(" vs. "); return (jsx_runtime_1.jsxs(react_1.Stack, { children: [jsx_runtime_1.jsx(react_1.StackItem, { children: jsx_runtime_1.jsx("h2", { children: teamTitle }, void 0) }, void 0), jsx_runtime_1.jsx(react_1.StackItem, { children: jsx_runtime_1.jsxs("table", Object.assign({ className: classNames.table }, { children: [jsx_runtime_1.jsx("thead", Object.assign({ className: classNames.playerRow }, { children: jsx_runtime_1.jsx("tr", { children: renderHeader(game, classNames) }, void 0) }), void 0), jsx_runtime_1.jsx("tbody", { children: cyclesRows }, void 0)] }), void 0) }, void 0)] }, void 0)); } }, void 0)); }); function getUnformattedAnswer(game, answer) { // Ignore alternate answers and remove all formatting from the primary answer const alternateIndex = answer.indexOf("["); if (alternateIndex >= 0) { answer = answer.substring(0, alternateIndex).trim(); } const text = FormattedTextParser.parseFormattedText(answer, { pronunciationGuideMarkers: game.gameFormat.pronunciationGuideMarkers, }) .map((line) => line.text) .join(""); return text; } function renderBonusCell(cycle, teamName, cycleIndex, classNames) { if (cycle.bonusAnswer && cycle.bonusAnswer.receivingTeamName === teamName) { // Go through each part, show check or ✓✗ const lines = []; let bonusTotal = 0; for (let i = 0; i < cycle.bonusAnswer.parts.length; i++) { const part = cycle.bonusAnswer.parts[i]; lines.push(part.points > 0 ? (jsx_runtime_1.jsx("span", Object.assign({ className: classNames.correctBonus }, { children: "\u2713" }), `Bonus_${cycleIndex}_${teamName}_${i}`)) : (jsx_runtime_1.jsx("span", Object.assign({ className: classNames.wrongBonus }, { children: "\u2717" }), `Bonus_${cycleIndex}_${teamName}_${i}`))); bonusTotal += part.points; } lines.push(jsx_runtime_1.jsxs("span", { children: [" ", bonusTotal] }, `Bonus_${cycleIndex}_${teamName}_Total`)); return (jsx_runtime_1.jsx("td", Object.assign({ className: `${classNames.bonusCell} ${classNames.tableCell}` }, { children: lines }), `Bonus_${cycleIndex}_${teamName}`)); } return jsx_runtime_1.jsx("td", { className: `${classNames.bonusCell} ${classNames.tableCell}` }, `Bonus_${cycleIndex}_${teamName}`); } function renderHeader(game, classNames) { // header should be // first team players; Bonus; Total ; TU ; second team players; Bonus; Total // Because MODAQ supports non-three part bonuses we need to do checks and Xs in one cell const headers = []; for (let i = 0; i < game.teamNames.length; i++) { const teamName = game.teamNames[i]; const players = game.getPlayers(teamName); for (const player of players) { headers.push(jsx_runtime_1.jsx("th", Object.assign({ className: classNames.tableHeader }, { children: player.name }), `${teamName}_${player.name}`)); } headers.push(jsx_runtime_1.jsx("th", Object.assign({ className: classNames.tableHeader }, { children: "Bonus" }), `thBonus_${i}`)); headers.push(jsx_runtime_1.jsx("th", Object.assign({ className: classNames.tableHeader }, { children: "Total" }), `thTotal_${i}`)); // Don't include the question counter on the last row (no teams after it to follow along with) if (i < game.teamNames.length - 1) { headers.push(jsx_runtime_1.jsx("th", Object.assign({ className: classNames.tableHeader }, { children: jsx_runtime_1.jsx("h3", Object.assign({ className: classNames.tuLabel }, { children: "TU" }), void 0) }), "thTU")); } } return headers; } function renderPlayerCell(game, player, cycle, activeTeamPlayers, playerToStatlineMap, cycleIndex, classNames) { var _a, _b; // if this is too inefficient (because we check all players for the correct buzz), move to using a map. This means // we need existing cells that we can overwrite. From testing, this seems to be fine. if (cycle.correctBuzz && PlayerUtils.playersEqual(cycle.correctBuzz.marker.player, player)) { const correctPoints = cycle.correctBuzz.marker.points; const answer = getUnformattedAnswer(game, game.packet.tossups[cycle.correctBuzz.tossupIndex].answer); // We know this exists since we initialized it earlier const statlineMap = playerToStatlineMap.get(player); const pointValueCount = (_a = statlineMap.get(correctPoints)) !== null && _a !== void 0 ? _a : 0; statlineMap.set(correctPoints, pointValueCount + 1); return (jsx_runtime_1.jsx("td", Object.assign({ className: classNames.tableCell, title: `TU on "${answer}" at word ${cycle.correctBuzz.marker.position + 1} correct for ${correctPoints} points` }, { children: correctPoints }), `correct_${cycleIndex}`)); } else if (cycle.wrongBuzzes) { const wrongBuzz = cycle.wrongBuzzes.find((buzz) => PlayerUtils.playersEqual(buzz.marker.player, player)); if (wrongBuzz) { const wrongPoints = wrongBuzz.marker.points; const answer = getUnformattedAnswer(game, game.packet.tossups[wrongBuzz.tossupIndex].answer); // We know this exists since we initialized it earlier const statlineMap = playerToStatlineMap.get(player); const pointValueCount = (_b = statlineMap.get(wrongPoints)) !== null && _b !== void 0 ? _b : 0; statlineMap.set(wrongPoints, pointValueCount + 1); return (jsx_runtime_1.jsx("td", Object.assign({ className: classNames.tableCell, title: `TU on "${answer}" at word ${wrongBuzz.marker.position + 1} incorrect for ${wrongPoints} points` }, { children: wrongPoints }), `wrong_${player.teamName}_${player.name}_${cycleIndex}`)); } } let cellClassName = classNames.tableCell; if (!activeTeamPlayers.has(player)) { cellClassName += " " + classNames.inactivePlayerCell; } return jsx_runtime_1.jsx("td", { className: cellClassName }, `empty_${player.teamName}_${player.name}`); } function renderStatlineRow(game, teamToPlayerMap, playerToStatlineMap, classNames) { var _a; const statlineCells = []; let allowedTuPoints = [10]; if (game.gameFormat.negValue < 0) { allowedTuPoints.push(game.gameFormat.negValue); } if (game.gameFormat.powers) { // powers is already in descending order, so no need to sort allowedTuPoints = game.gameFormat.powers.map((marker) => marker.points).concat(allowedTuPoints); } let isFirstTeamName = true; for (let i = 0; i < game.teamNames.length; i++) { const teamName = game.teamNames[i]; if (!isFirstTeamName) { // Add filler cell for TU column statlineCells.push(jsx_runtime_1.jsx("td", Object.assign({ className: `${classNames.tableCell} ${classNames.tuNumber}` }, { children: "END" }), "END")); } else { isFirstTeamName = false; } // We initialized this before, we'll always have the array const teamPlayers = teamToPlayerMap.get(teamName); let tuTotal = 0; for (const player of teamPlayers) { const statlineMap = playerToStatlineMap.get(player); let totalPoints = 0; const statline = []; // Order should be based on superpower/power/gets/negs, then the total for (const value of allowedTuPoints) { const valueCount = (_a = statlineMap.get(value)) !== null && _a !== void 0 ? _a : 0; statline.push(valueCount); totalPoints += valueCount * value; } statlineCells.push(jsx_runtime_1.jsxs("td", Object.assign({ className: classNames.tableCell }, { children: [totalPoints, " (", statline.join("/"), ")"] }), `stat_${teamName}_${player.name}`)); tuTotal += totalPoints; } // Bonus total and total score cells const teamTotal = game.finalScore[i]; statlineCells.push(jsx_runtime_1.jsx("td", Object.assign({ className: classNames.tableCell }, { children: teamTotal - tuTotal }), `EndBonus_${teamName}`)); statlineCells.push(jsx_runtime_1.jsx("td", Object.assign({ className: `${classNames.tableCell} ${classNames.totalScoreCell}` }, { children: teamTotal }), `EndTotal_${teamName}`)); } // tr needs a class to make the top border solid return (jsx_runtime_1.jsx("tr", Object.assign({ className: classNames.statlineRow }, { children: statlineCells }), "statline")); } const getClassNames = react_1.memoizeFunction((theme) => { var _a; return react_1.mergeStyleSets({ bonusCell: { borderLeft: "1px solid", borderRight: "1px solid", }, correctBonus: { color: theme ? theme.palette.tealLight : "rbg(0, 128, 128)", }, cycleRow: { borderLeft: "1px solid", borderRight: "1px solid", }, inactivePlayerCell: { backgroundColor: (_a = theme === null || theme === void 0 ? void 0 : theme.palette.neutralPrimary) !== null && _a !== void 0 ? _a : "black", }, playerRow: { borderBottom: "1px solid", margin: 0, }, statlineRow: { border: "1px solid", // Have a high top-border to make it clear that this isn't another cycle row borderTop: "20px solid", }, table: { borderCollapse: "collapse", }, tableCell: { border: "1px dotted", textAlign: "center", padding: "0 2px", }, tableHeader: { padding: "0em 0.5em", }, totalScoreCell: { fontWeight: 500, }, tuLabel: { marginBottom: 0, }, tuNumber: { textAlign: "center", fontWeight: 700, borderLeft: "1px solid", borderRight: "1px solid", }, wrongBonus: { color: theme ? theme.palette.red : "rbg(128, 0, 0)", }, }); }); //# sourceMappingURL=ScoresheetDialog.js.map