@ttt-productions/q-core 0.1.0 → 0.1.1
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.
- package/dist/constants/business.js.map +1 -1
- package/dist/constants/sports.js.map +1 -1
- package/dist/constants/storage-keys.js.map +1 -1
- package/dist/doc-schemas/catalogs.js.map +1 -1
- package/dist/doc-schemas/game-location.js.map +1 -1
- package/dist/doc-schemas/game-stats.js.map +1 -1
- package/dist/doc-schemas/league-team.js.map +1 -1
- package/dist/doc-schemas/pending-media.js.map +1 -1
- package/dist/doc-schemas/registry.js.map +1 -1
- package/dist/doc-schemas/sport-game.js.map +1 -1
- package/dist/doc-schemas/system.js.map +1 -1
- package/dist/doc-schemas/user-profile.js.map +1 -1
- package/dist/doc-schemas/waiver-template.js.map +1 -1
- package/dist/doc-schemas/waivers-and-hardware.js.map +1 -1
- package/dist/ids/index.js.map +1 -1
- package/dist/ids/minted-ids.js.map +1 -1
- package/dist/ids/stats-operation-ids.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/media/file-origins.js.map +1 -1
- package/dist/paths/collection-refs.js.map +1 -1
- package/dist/paths/collections.js.map +1 -1
- package/dist/paths/path-builders.js.map +1 -1
- package/dist/paths/utils.js.map +1 -1
- package/dist/schemas/account-settings.js.map +1 -1
- package/dist/schemas/atoms.js.map +1 -1
- package/dist/schemas/awards.js.map +1 -1
- package/dist/schemas/hardware-loan-contracts.js.map +1 -1
- package/dist/schemas/schedules.js.map +1 -1
- package/dist/schemas/scoring.js.map +1 -1
- package/dist/schemas/stats.js.map +1 -1
- package/dist/schemas/team-joining.js.map +1 -1
- package/dist/schemas/teams.js.map +1 -1
- package/dist/schemas/uploads.js.map +1 -1
- package/dist/schemas/users.js.map +1 -1
- package/dist/sports/basketball-scoring.js.map +1 -1
- package/dist/sports/basketball.js.map +1 -1
- package/dist/sports/bulk-entry.js.map +1 -1
- package/dist/sports/contribution.js.map +1 -1
- package/dist/sports/curling-scoring.js.map +1 -1
- package/dist/sports/finalized-record.js.map +1 -1
- package/dist/sports/football-scoring.js.map +1 -1
- package/dist/sports/index.js.map +1 -1
- package/dist/sports/rankings.js.map +1 -1
- package/dist/sports/rollup-check.js.map +1 -1
- package/dist/sports/scoring.js.map +1 -1
- package/dist/sports/store-driver.js.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/types/notifications.js.map +1 -1
- package/dist/types/picture-owners.js.map +1 -1
- package/dist/types/sports.js.map +1 -1
- package/dist/types/stats.js.map +1 -1
- package/dist/types/unions.js.map +1 -1
- package/dist/utils/account-status.js.map +1 -1
- package/dist/utils/app-config.js.map +1 -1
- package/dist/utils/app-routes.js.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/league-calendar.js.map +1 -1
- package/dist/utils/league-mode.js.map +1 -1
- package/dist/utils/team-member-actions.js.map +1 -1
- package/package.json +90 -90
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basketball.js","sourceRoot":"","sources":["../../src/sports/basketball.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,gGAAgG;AAChG,2BAA2B;AAE3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAGvE,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,EAC/B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAG7G,4FAA4F;AAC5F,MAAM,MAAM,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AAC7E,MAAM,eAAe,GAAG,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AACtG,MAAM,QAAQ,GAAG,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AACnF,MAAM,iBAAiB,GAAG,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AAC5G,MAAM,OAAO,GAAG,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AAChF,MAAM,gBAAgB,GAAG,EAAE,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,KAAK,EAAW,CAAC;AACzG,0FAA0F;AAC1F,MAAM,cAAc,GAAG,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,IAAI,EAAW,CAAC;AACnG,MAAM,uBAAuB,GAAG,EAAE,EAAE,EAAE,sBAAsB,EAAE,KAAK,EAAE,yBAAyB,EAAE,UAAU,EAAE,MAAM,EAAW,CAAC;AAE9H,MAAM,SAAS,GAA0B,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;AAEtF,MAAM,mBAAmB,GAAkD;IACzE,mBAAmB;IACnB,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE;IACnF,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,EAAE;IACzF,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE;IACtF,EAAE,GAAG,cAAc,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,MAAM,EAAE;CAC3G,CAAC;AAEF,MAAM,iBAAiB,GAAiD;IACtE,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;IACzD,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE;IAC7D,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;IAC3D,EAAE,GAAG,cAAc,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE;CACzE,CAAC;AAEF,MAAM,iBAAiB,GAAqD;IAC1E,GAAG,wBAAwB;IAC3B,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,EAAE;IACzF,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE;CACvF,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAA+B;IAC5D,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,wBAAwB,CAAC,UAAU;IAC7C,UAAU,EAAE,+BAA+B;IAC3C,YAAY,EAAE,yBAAyB;IACvC,cAAc,EAAE,2BAA2B;IAC3C,UAAU,EAAE,IAAI;IAChB,kBAAkB,EAAE,IAAI;IACxB,WAAW,EAAE,kBAAkB,CAAC,UAAU;IAC1C,GAAG,kBAAkB;IACrB,OAAO,EAAE;QACP,KAAK,EAAE,YAAY;QACnB,OAAO,EAAE,gBAAgB;QACzB,eAAe,EAAE,0BAA0B,CAAC,mBAAmB,CAAC;QAChE,WAAW,EAAE,uBAAuB;QACpC,WAAW,EAAE,uBAAuB;QACpC,aAAa,EAAE,uBAAuB;QACtC,UAAU,EAAE,qBAAqB;QACjC,YAAY,EAAE,wBAAwB;KACvC;IACD,OAAO,EAAE;QACP,YAAY,EAAE;YACZ,OAAO,EAAE,mBAAmB;YAC5B,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE;gBACP;oBACE,EAAE,EAAE,QAAQ;oBACZ,KAAK,EAAE,QAAQ;oBACf,SAAS,EAAE,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,cAAc,CAAC,EAAE,CAAC;iBAC3F;gBACD;oBACE,EAAE,EAAE,SAAS;oBACb,KAAK,EAAE,UAAU;oBACjB,SAAS,EAAE;wBACT,mBAAmB,CAAC,EAAE;wBACtB,eAAe,CAAC,EAAE;wBAClB,iBAAiB,CAAC,EAAE;wBACpB,gBAAgB,CAAC,EAAE;wBACnB,uBAAuB,CAAC,EAAE;qBAC3B;iBACF;aACF;SACF;QACD,UAAU,EAAE;YACV,OAAO,EAAE,iBAAiB;YAC1B,WAAW,EAAE,SAAS;YACtB,aAAa,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,cAAc,CAAC,EAAE,CAAC;SACvE;QACD,UAAU,EAAE;YACV,OAAO,EAAE,iBAAiB;YAC1B,WAAW,EAAE;gBACX,EAAE,QAAQ,EAAE,uBAAuB,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;gBAChE,EAAE,QAAQ,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;gBACjE,EAAE,QAAQ,EAAE,uBAAuB,CAAC,iBAAiB,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;aAC9E;YACD,aAAa,EAAE;gBACb,uBAAuB,CAAC,IAAI,CAAC,EAAE;gBAC/B,uBAAuB,CAAC,MAAM,CAAC,EAAE;gBACjC,uBAAuB,CAAC,SAAS,CAAC,EAAE;gBACpC,uBAAuB,CAAC,aAAa,CAAC,EAAE;gBACxC,uBAAuB,CAAC,iBAAiB,CAAC,EAAE;gBAC5C,QAAQ,CAAC,EAAE;gBACX,OAAO,CAAC,EAAE;aACX;SACF;KACF;IACD,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC;QAChG,WAAW,EAAE,EAAE;KAChB,CAAC;IACF,SAAS,EAAE,qBAAqB;CACjC,CAAC","sourcesContent":["// The basketball adapter: player-scored — a team's score is the sum of its players' points — with\r\n// a hall of fame and a historical baseline. Its scoring, display, and bulk-entry members are in\r\n// ./basketball-scoring.ts.\r\n\r\nimport { SPORT_LINE_SCHEMAS } from '../doc-schemas/aggregate-stats.js';\r\nimport type { BasketballPlayerTotals, BasketballTeamSeasonStats } from '../doc-schemas/aggregate-stats.js';\r\nimport type { BasketballPlayerStats } from '../doc-schemas/game-stats.js';\r\nimport {\r\n BASKETBALL_PLAYER_STAT_KEYS,\r\n BASKETBALL_STAT_EVENT_TYPE_KEYS,\r\n BASKETBALL_TEAM_STAT_KEYS,\r\n SPORT_KEY_BY_LEAGUE_TYPE,\r\n} from '../types/sports.js';\r\nimport {\r\n BASKETBALL_BULK_ENTRY,\r\n BASKETBALL_EVENT_LABELS,\r\n BASKETBALL_FIELD_LABELS,\r\n BASKETBALL_SCOREBOARD,\r\n BASKETBALL_SCORING,\r\n BASKETBALL_SCORING_PANEL,\r\n describeBasketballEvent,\r\n emptyBasketballTeamLine,\r\n} from './basketball-scoring.js';\r\nimport type { ColumnSort, StatColumn } from './column-catalog.js';\r\nimport { lifetimeSummaryFromColumns } from './lifetime-summary.js';\r\nimport { GAMES_PLAYED_COLUMN, TEAM_SEASON_BASE_COLUMN, TEAM_SEASON_BASE_COLUMNS } from './shared-columns.js';\r\nimport type { SportAdapter } from './sport-adapter.js';\r\n\r\n// Each figure's copy, declared once and shared by the player and team columns that show it.\r\nconst POINTS = { id: 'points', label: 'Points', shortLabel: 'PTS' } as const;\r\nconst POINTS_PER_GAME = { id: 'pointsPerGame', label: 'Points per game', shortLabel: 'PPG' } as const;\r\nconst REBOUNDS = { id: 'rebounds', label: 'Rebounds', shortLabel: 'REB' } as const;\r\nconst REBOUNDS_PER_GAME = { id: 'reboundsPerGame', label: 'Rebounds per game', shortLabel: 'RPG' } as const;\r\nconst ASSISTS = { id: 'assists', label: 'Assists', shortLabel: 'AST' } as const;\r\nconst ASSISTS_PER_GAME = { id: 'assistsPerGame', label: 'Assists per game', shortLabel: 'APG' } as const;\r\n// FP is stored on every player line, never computed for display (`deriveLine` writes it).\r\nconst FANTASY_POINTS = { id: 'fantasyPoints', label: 'Fantasy points', shortLabel: 'FP' } as const;\r\nconst FANTASY_POINTS_PER_GAME = { id: 'fantasyPointsPerGame', label: 'Fantasy points per game', shortLabel: 'FP/G' } as const;\r\n\r\nconst BY_POINTS: readonly ColumnSort[] = [{ columnId: POINTS.id, direction: 'desc' }];\r\n\r\nconst playerTotalsColumns: readonly StatColumn<BasketballPlayerTotals>[] = [\r\n GAMES_PLAYED_COLUMN,\r\n { ...POINTS, value: (line) => line.points, perGame: POINTS_PER_GAME, sort: 'desc' },\r\n { ...REBOUNDS, value: (line) => line.rebounds, perGame: REBOUNDS_PER_GAME, sort: 'desc' },\r\n { ...ASSISTS, value: (line) => line.assists, perGame: ASSISTS_PER_GAME, sort: 'desc' },\r\n { ...FANTASY_POINTS, value: (line) => line.fantasyPoints, perGame: FANTASY_POINTS_PER_GAME, sort: 'desc' },\r\n];\r\n\r\nconst gamePlayerColumns: readonly StatColumn<BasketballPlayerStats>[] = [\r\n { ...POINTS, value: (line) => line.points, sort: 'desc' },\r\n { ...REBOUNDS, value: (line) => line.rebounds, sort: 'desc' },\r\n { ...ASSISTS, value: (line) => line.assists, sort: 'desc' },\r\n { ...FANTASY_POINTS, value: (line) => line.fantasyPoints, sort: 'desc' },\r\n];\r\n\r\nconst teamSeasonColumns: readonly StatColumn<BasketballTeamSeasonStats>[] = [\r\n ...TEAM_SEASON_BASE_COLUMNS,\r\n { ...REBOUNDS, value: (line) => line.rebounds, perGame: REBOUNDS_PER_GAME, sort: 'desc' },\r\n { ...ASSISTS, value: (line) => line.assists, perGame: ASSISTS_PER_GAME, sort: 'desc' },\r\n];\r\n\r\nexport const BASKETBALL_ADAPTER: SportAdapter<'Basketball'> = {\r\n leagueType: 'Basketball',\r\n sportKey: SPORT_KEY_BY_LEAGUE_TYPE.Basketball,\r\n eventTypes: BASKETBALL_STAT_EVENT_TYPE_KEYS,\r\n teamStatKeys: BASKETBALL_TEAM_STAT_KEYS,\r\n playerStatKeys: BASKETBALL_PLAYER_STAT_KEYS,\r\n hallOfFame: true,\r\n historicalBaseline: true,\r\n lineSchemas: SPORT_LINE_SCHEMAS.Basketball,\r\n ...BASKETBALL_SCORING,\r\n display: {\r\n label: 'Basketball',\r\n iconKey: 'basketballBall',\r\n lifetimeSummary: lifetimeSummaryFromColumns(playerTotalsColumns),\r\n eventLabels: BASKETBALL_EVENT_LABELS,\r\n fieldLabels: BASKETBALL_FIELD_LABELS,\r\n describeEvent: describeBasketballEvent,\r\n scoreboard: BASKETBALL_SCOREBOARD,\r\n scoringPanel: BASKETBALL_SCORING_PANEL,\r\n },\r\n columns: {\r\n playerTotals: {\r\n columns: playerTotalsColumns,\r\n defaultSort: BY_POINTS,\r\n presets: [\r\n {\r\n id: 'totals',\r\n label: 'Totals',\r\n columnIds: [GAMES_PLAYED_COLUMN.id, POINTS.id, REBOUNDS.id, ASSISTS.id, FANTASY_POINTS.id],\r\n },\r\n {\r\n id: 'perGame',\r\n label: 'Per game',\r\n columnIds: [\r\n GAMES_PLAYED_COLUMN.id,\r\n POINTS_PER_GAME.id,\r\n REBOUNDS_PER_GAME.id,\r\n ASSISTS_PER_GAME.id,\r\n FANTASY_POINTS_PER_GAME.id,\r\n ],\r\n },\r\n ],\r\n },\r\n gamePlayer: {\r\n columns: gamePlayerColumns,\r\n defaultSort: BY_POINTS,\r\n defaultLayout: [POINTS.id, REBOUNDS.id, ASSISTS.id, FANTASY_POINTS.id],\r\n },\r\n teamSeason: {\r\n columns: teamSeasonColumns,\r\n defaultSort: [\r\n { columnId: TEAM_SEASON_BASE_COLUMN.wins.id, direction: 'desc' },\r\n { columnId: TEAM_SEASON_BASE_COLUMN.losses.id, direction: 'asc' },\r\n { columnId: TEAM_SEASON_BASE_COLUMN.pointDifferential.id, direction: 'desc' },\r\n ],\r\n defaultLayout: [\r\n TEAM_SEASON_BASE_COLUMN.wins.id,\r\n TEAM_SEASON_BASE_COLUMN.losses.id,\r\n TEAM_SEASON_BASE_COLUMN.pointsFor.id,\r\n TEAM_SEASON_BASE_COLUMN.pointsAgainst.id,\r\n TEAM_SEASON_BASE_COLUMN.pointDifferential.id,\r\n REBOUNDS.id,\r\n ASSISTS.id,\r\n ],\r\n },\r\n },\r\n emptyStats: (game) => ({\r\n teamStats: Object.fromEntries(game.teamIds.map((teamId) => [teamId, emptyBasketballTeamLine()])),\r\n playerStats: {},\r\n }),\r\n bulkEntry: BASKETBALL_BULK_ENTRY,\r\n};\r\n"]}
|
|
1
|
+
{"version":3,"file":"basketball.js","sourceRoot":"","sources":["../../src/sports/basketball.ts"],"names":[],"mappings":"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","sourcesContent":["// The basketball adapter: player-scored — a team's score is the sum of its players' points — with\n// a hall of fame and a historical baseline. Its scoring, display, and bulk-entry members are in\n// ./basketball-scoring.ts.\n\nimport { SPORT_LINE_SCHEMAS } from '../doc-schemas/aggregate-stats.js';\nimport type { BasketballPlayerTotals, BasketballTeamSeasonStats } from '../doc-schemas/aggregate-stats.js';\nimport type { BasketballPlayerStats } from '../doc-schemas/game-stats.js';\nimport {\n BASKETBALL_PLAYER_STAT_KEYS,\n BASKETBALL_STAT_EVENT_TYPE_KEYS,\n BASKETBALL_TEAM_STAT_KEYS,\n SPORT_KEY_BY_LEAGUE_TYPE,\n} from '../types/sports.js';\nimport {\n BASKETBALL_BULK_ENTRY,\n BASKETBALL_EVENT_LABELS,\n BASKETBALL_FIELD_LABELS,\n BASKETBALL_SCOREBOARD,\n BASKETBALL_SCORING,\n BASKETBALL_SCORING_PANEL,\n describeBasketballEvent,\n emptyBasketballTeamLine,\n} from './basketball-scoring.js';\nimport type { ColumnSort, StatColumn } from './column-catalog.js';\nimport { lifetimeSummaryFromColumns } from './lifetime-summary.js';\nimport { GAMES_PLAYED_COLUMN, TEAM_SEASON_BASE_COLUMN, TEAM_SEASON_BASE_COLUMNS } from './shared-columns.js';\nimport type { SportAdapter } from './sport-adapter.js';\n\n// Each figure's copy, declared once and shared by the player and team columns that show it.\nconst POINTS = { id: 'points', label: 'Points', shortLabel: 'PTS' } as const;\nconst POINTS_PER_GAME = { id: 'pointsPerGame', label: 'Points per game', shortLabel: 'PPG' } as const;\nconst REBOUNDS = { id: 'rebounds', label: 'Rebounds', shortLabel: 'REB' } as const;\nconst REBOUNDS_PER_GAME = { id: 'reboundsPerGame', label: 'Rebounds per game', shortLabel: 'RPG' } as const;\nconst ASSISTS = { id: 'assists', label: 'Assists', shortLabel: 'AST' } as const;\nconst ASSISTS_PER_GAME = { id: 'assistsPerGame', label: 'Assists per game', shortLabel: 'APG' } as const;\n// FP is stored on every player line, never computed for display (`deriveLine` writes it).\nconst FANTASY_POINTS = { id: 'fantasyPoints', label: 'Fantasy points', shortLabel: 'FP' } as const;\nconst FANTASY_POINTS_PER_GAME = { id: 'fantasyPointsPerGame', label: 'Fantasy points per game', shortLabel: 'FP/G' } as const;\n\nconst BY_POINTS: readonly ColumnSort[] = [{ columnId: POINTS.id, direction: 'desc' }];\n\nconst playerTotalsColumns: readonly StatColumn<BasketballPlayerTotals>[] = [\n GAMES_PLAYED_COLUMN,\n { ...POINTS, value: (line) => line.points, perGame: POINTS_PER_GAME, sort: 'desc' },\n { ...REBOUNDS, value: (line) => line.rebounds, perGame: REBOUNDS_PER_GAME, sort: 'desc' },\n { ...ASSISTS, value: (line) => line.assists, perGame: ASSISTS_PER_GAME, sort: 'desc' },\n { ...FANTASY_POINTS, value: (line) => line.fantasyPoints, perGame: FANTASY_POINTS_PER_GAME, sort: 'desc' },\n];\n\nconst gamePlayerColumns: readonly StatColumn<BasketballPlayerStats>[] = [\n { ...POINTS, value: (line) => line.points, sort: 'desc' },\n { ...REBOUNDS, value: (line) => line.rebounds, sort: 'desc' },\n { ...ASSISTS, value: (line) => line.assists, sort: 'desc' },\n { ...FANTASY_POINTS, value: (line) => line.fantasyPoints, sort: 'desc' },\n];\n\nconst teamSeasonColumns: readonly StatColumn<BasketballTeamSeasonStats>[] = [\n ...TEAM_SEASON_BASE_COLUMNS,\n { ...REBOUNDS, value: (line) => line.rebounds, perGame: REBOUNDS_PER_GAME, sort: 'desc' },\n { ...ASSISTS, value: (line) => line.assists, perGame: ASSISTS_PER_GAME, sort: 'desc' },\n];\n\nexport const BASKETBALL_ADAPTER: SportAdapter<'Basketball'> = {\n leagueType: 'Basketball',\n sportKey: SPORT_KEY_BY_LEAGUE_TYPE.Basketball,\n eventTypes: BASKETBALL_STAT_EVENT_TYPE_KEYS,\n teamStatKeys: BASKETBALL_TEAM_STAT_KEYS,\n playerStatKeys: BASKETBALL_PLAYER_STAT_KEYS,\n hallOfFame: true,\n historicalBaseline: true,\n lineSchemas: SPORT_LINE_SCHEMAS.Basketball,\n ...BASKETBALL_SCORING,\n display: {\n label: 'Basketball',\n iconKey: 'basketballBall',\n lifetimeSummary: lifetimeSummaryFromColumns(playerTotalsColumns),\n eventLabels: BASKETBALL_EVENT_LABELS,\n fieldLabels: BASKETBALL_FIELD_LABELS,\n describeEvent: describeBasketballEvent,\n scoreboard: BASKETBALL_SCOREBOARD,\n scoringPanel: BASKETBALL_SCORING_PANEL,\n },\n columns: {\n playerTotals: {\n columns: playerTotalsColumns,\n defaultSort: BY_POINTS,\n presets: [\n {\n id: 'totals',\n label: 'Totals',\n columnIds: [GAMES_PLAYED_COLUMN.id, POINTS.id, REBOUNDS.id, ASSISTS.id, FANTASY_POINTS.id],\n },\n {\n id: 'perGame',\n label: 'Per game',\n columnIds: [\n GAMES_PLAYED_COLUMN.id,\n POINTS_PER_GAME.id,\n REBOUNDS_PER_GAME.id,\n ASSISTS_PER_GAME.id,\n FANTASY_POINTS_PER_GAME.id,\n ],\n },\n ],\n },\n gamePlayer: {\n columns: gamePlayerColumns,\n defaultSort: BY_POINTS,\n defaultLayout: [POINTS.id, REBOUNDS.id, ASSISTS.id, FANTASY_POINTS.id],\n },\n teamSeason: {\n columns: teamSeasonColumns,\n defaultSort: [\n { columnId: TEAM_SEASON_BASE_COLUMN.wins.id, direction: 'desc' },\n { columnId: TEAM_SEASON_BASE_COLUMN.losses.id, direction: 'asc' },\n { columnId: TEAM_SEASON_BASE_COLUMN.pointDifferential.id, direction: 'desc' },\n ],\n defaultLayout: [\n TEAM_SEASON_BASE_COLUMN.wins.id,\n TEAM_SEASON_BASE_COLUMN.losses.id,\n TEAM_SEASON_BASE_COLUMN.pointsFor.id,\n TEAM_SEASON_BASE_COLUMN.pointsAgainst.id,\n TEAM_SEASON_BASE_COLUMN.pointDifferential.id,\n REBOUNDS.id,\n ASSISTS.id,\n ],\n },\n },\n emptyStats: (game) => ({\n teamStats: Object.fromEntries(game.teamIds.map((teamId) => [teamId, emptyBasketballTeamLine()])),\n playerStats: {},\n }),\n bulkEntry: BASKETBALL_BULK_ENTRY,\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-entry.js","sourceRoot":"","sources":["../../src/sports/bulk-entry.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,oGAAoG;AACpG,qGAAqG;AACrG,sDAAsD;AAEtD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8DtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,4BAA4B,GACvC,6FAA6F,CAAC;AAEhG,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,qCAAqC,CAAC;AAE9E;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CACzC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,EAC/C,wBAAwB,CACzB,CAAC","sourcesContent":["// A sport's bulk entry: the form the shared bulk-entry dialog renders for the sport's kind, and the\
|
|
1
|
+
{"version":3,"file":"bulk-entry.js","sourceRoot":"","sources":["../../src/sports/bulk-entry.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,oGAAoG;AACpG,qGAAqG;AACrG,sDAAsD;AAEtD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8DtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,4BAA4B,GACvC,6FAA6F,CAAC;AAEhG,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,qCAAqC,CAAC;AAE9E;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CACzC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,EAC/C,wBAAwB,CACzB,CAAC","sourcesContent":["// A sport's bulk entry: the form the shared bulk-entry dialog renders for the sport's kind, and the\n// events a submission becomes. The form's rules are built from the sport's constants, and every one\n// of them is also an event rule the adapter's `validate` enforces, because the callable sees events,\n// never form values, and captains use it (SPORT-006).\n\nimport { z } from 'zod';\nimport { countSchema } from '../doc-schemas/atoms.js';\nimport type { GameStats } from '../doc-schemas/sport-game.js';\nimport type { BulkEntryKind } from '../types/stats.js';\nimport type { SportStatEventBody, StatVoidBody } from './scoring.js';\nimport type { GameTeamIds } from './sport-adapter.js';\n\n/** A recorded round's live event — one no void voids — so an edit of that round can void it. */\nexport interface LiveRoundEvent {\n readonly eventId: string;\n readonly roundNumber: number;\n}\n\n/** What the form edits: the game, the scorer's current state, and the game's roster. */\nexport interface BulkEntryContext {\n readonly game: GameTeamIds;\n /** The scorer's optimistic rollup, the queue's pending events applied. */\n readonly state: GameStats;\n /** Every roster entry of the game, grouped by team in the order the form lists them. */\n readonly rosterPlayerIds: readonly string[];\n readonly absentRosterPlayerIds: readonly string[];\n /** For a sport played in rounds, the game's unvoided round events; empty for any other sport. */\n readonly liveRoundEvents: readonly LiveRoundEvent[];\n}\n\n/**\n * One event of a submission, in batch order: a sport event's type and payload, or the target of a\n * void. At submit — the tap — the dialog mints each sport event's id and stamps its `recordedAt`, and\n * the queue sends the drafts as one unit it never splits across calls.\n */\nexport type StatEventDraft = SportStatEventBody | StatVoidBody;\n\nexport interface BulkEntryForm<K extends BulkEntryKind, Values> {\n readonly kind: K;\n /** The form's resolver: its rules for this game and state. */\n readonly formSchema: (context: BulkEntryContext) => z.ZodType<Values>;\n readonly formDefaults: (context: BulkEntryContext) => Values;\n /** The drafts a submission becomes; empty when nothing changed. */\n readonly toEvents: (values: Values, context: BulkEntryContext) => readonly StatEventDraft[];\n}\n\n/** Each roster entry's player counters, by roster entry id. */\nexport interface PlayerLinesValues {\n readonly lines: Readonly<Record<string, Readonly<Record<string, number>>>>;\n}\n\n/** Each team's final score, by team id. */\nexport interface TeamScoresValues {\n readonly scores: Readonly<Record<string, number>>;\n}\n\n/** Each round, first to last: every team's points by team id, `null` where the round is not recorded. */\nexport interface RoundsValues {\n readonly rounds: readonly Readonly<Record<string, number | null>>[];\n}\n\nexport type PlayerLinesBulkEntry = BulkEntryForm<'playerLines', PlayerLinesValues>;\nexport type TeamScoresBulkEntry = BulkEntryForm<'teamScores', TeamScoresValues>;\nexport type RoundsBulkEntry = BulkEntryForm<'rounds', RoundsValues>;\n\n/** The dialog picks its form body by `kind`, from an exhaustive map over the kind union. */\nexport type BulkEntry = PlayerLinesBulkEntry | TeamScoresBulkEntry | RoundsBulkEntry;\n\n/** The form's refusal when a submission would pass the per-write event cap. */\nexport const BULK_ENTRY_TOO_LARGE_MESSAGE =\n 'This entry changes more stats than one save can hold. Save part of it, then enter the rest.';\n\n/** The form's refusal of a value that is not a whole number of zero or more. */\nexport const BULK_ENTRY_COUNT_MESSAGE = 'Enter a whole number, zero or more.';\n\n/**\n * Every bulk-entry cell that holds a count: the count atom's check, answered with the one message the\n * form shows for any failure of it — which the atom's own issues cannot carry.\n */\nexport const bulkEntryCountField = z.custom<number>(\n (value) => countSchema.safeParse(value).success,\n BULK_ENTRY_COUNT_MESSAGE,\n);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contribution.js","sourceRoot":"","sources":["../../src/sports/contribution.ts"],"names":[],"mappings":"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","sourcesContent":["// The aggregate half of the sport adapter contract: who a completed game credits a game played, and\r\n// what the game adds to every derived destination (SPORT-201, SPORT-204, SPORT-207). Each sport\r\n// declares its mapping once, as a `ContributionSpec`; the two members built from it — `participation`\r\n// and `contribution` — are the only place a game's effect on standings, per-membership rows, lifetime\r\n// lines, and the hall of fame is decided. Finalize, reopen, a ghost link, every rebuild, validation,\r\n// and the week view all derive through them, so no two paths can disagree about what a game adds.\r\n//\r\n// Everything here is pure: no Firestore operation, no clock, no id minted (ARCH-202). The aggregate\r\n// writer in Functions applies a contribution; the browser sums contributions for the week view.\r\n//\r\n// A stored derived line that sums to nothing — every counter and games played zero — is the same as\r\n// no line at all, as an all-zero player line is in a game's rollup: a contribution leaves one out,\r\n// and applying a contribution removes a line it returns to zero. That is what makes finalize followed\r\n// by reopen leave every destination exactly as it was.\r\n\r\nimport type { GameStats } from '../doc-schemas/sport-game.js';\r\nimport { parseGameRosterPlayerId } from '../ids/compound-ids.js';\r\nimport { isGhostUserId } from '../ids/minted-ids.js';\r\nimport type { LeagueType } from '../types/sports.js';\r\nimport type { StatLineKind } from '../types/stats.js';\r\nimport { isAllZeroLine } from './scoring.js';\r\nimport type { StatLineValues } from './scoring.js';\r\n\r\n/**\r\n * A completed game as its finalized record holds it: the record's events replayed to lines, and the\r\n * first page's sport, teams, and participation snapshot. Finalize builds it from the game it is\r\n * finalizing; every later path builds it from the record, never from the game document (SPORT-204).\r\n */\r\nexport interface FinalizedGame {\r\n readonly leagueType: LeagueType;\r\n readonly teamIds: readonly string[];\r\n readonly stats: GameStats;\r\n readonly rosterPlayerIds: readonly string[];\r\n readonly absentRosterPlayerIds: readonly string[];\r\n readonly nonScoringRosterPlayerIds: readonly string[];\r\n}\r\n\r\n/**\r\n * What one completed game adds to each destination, before the caller's sign. Every line carries its\r\n * derived fields through the sport's `deriveLine`.\r\n */\r\nexport interface GameContribution {\r\n /** Per team id: the team's standings line (`teamSeason`). A team with no opponent has none. */\r\n readonly teamLines: Readonly<Record<string, StatLineValues>>;\r\n /** Per roster-entry id: that membership's row (`membershipRow`), credited to the team in its key. */\r\n readonly membershipRows: Readonly<Record<string, StatLineValues>>;\r\n /**\r\n * Per person who is not a ghost: the sum of that person's rows, games played included — what the\r\n * game adds to `lifetimeStats.{sportKey}` and, where the sport keeps one, the hall of fame\r\n * (`lifetime`). A person on both teams of the game counts both sides (SPORT-208 keeps ghosts out).\r\n */\r\n readonly personLines: Readonly<Record<string, StatLineValues>>;\r\n}\r\n\r\nexport const EMPTY_GAME_CONTRIBUTION: GameContribution = { teamLines: {}, membershipRows: {}, personLines: {} };\r\n\r\n/** One sport's aggregate mapping, declared once by its adapter (SPORT-207). */\r\nexport interface ContributionSpec {\r\n /** The counters of a player line that a membership row, a lifetime line, and the hall of fame sum. */\r\n readonly playerStatKeys: readonly string[];\r\n /** The player counters summed onto the team's standings line from the lines keyed to that team. */\r\n readonly teamSeasonPlayerSums: readonly string[];\r\n /**\r\n * Whether a present entry whose line holds nothing is credited no game played. Basketball's rule:\r\n * zero points, rebounds, and assists — zero points alone is not exclusion. Without it only absence\r\n * withholds a game played.\r\n */\r\n readonly zeroLineIsNonScoring: boolean;\r\n readonly deriveLine: (kind: StatLineKind, line: StatLineValues) => StatLineValues;\r\n}\r\n\r\n/** The value of a counter on a stored line, absent read as zero. */\r\nfunction counter(line: StatLineValues | undefined, field: string): number {\r\n const value = line?.[field];\r\n return typeof value === 'number' ? value : 0;\r\n}\r\n\r\n/** Whether every number a line holds is zero: a derived line that sums to nothing. */\r\nexport function isZeroStatLine(line: StatLineValues): boolean {\r\n return Object.values(line).every((value) => typeof value !== 'number' || value === 0);\r\n}\r\n\r\n/**\r\n * The games-played decision for every roster entry: the entries credited no game played. An absent\r\n * entry is never credited; under `zeroLineIsNonScoring` a present entry whose line holds nothing is\r\n * not either. Stored on the completed game as `nonScoringRosterPlayerIds` and read, never decided\r\n * again, by every later path (SPORT-204). Returned in roster order.\r\n */\r\nexport function decideParticipation(\r\n spec: Pick<ContributionSpec, 'playerStatKeys' | 'zeroLineIsNonScoring'>,\r\n stats: GameStats,\r\n rosterPlayerIds: readonly string[],\r\n absentRosterPlayerIds: readonly string[],\r\n): string[] {\r\n if (!spec.zeroLineIsNonScoring) return [];\r\n const absent = new Set(absentRosterPlayerIds);\r\n const playerStats = stats.playerStats as Readonly<Record<string, StatLineValues>>;\r\n return rosterPlayerIds.filter((entryId) => {\r\n if (absent.has(entryId)) return true;\r\n const line = playerStats[entryId];\r\n return line === undefined || isAllZeroLine(line, spec.playerStatKeys);\r\n });\r\n}\r\n\r\n/** Whether the stored decision credits the entry a game played. */\r\nexport function isCreditedGamePlayed(game: Pick<FinalizedGame, 'absentRosterPlayerIds' | 'nonScoringRosterPlayerIds'>, entryId: string): boolean {\r\n return !game.absentRosterPlayerIds.includes(entryId) && !game.nonScoringRosterPlayerIds.includes(entryId);\r\n}\r\n\r\n/** `left` with every number of `right` added `sign` times; a field only one holds is kept. */\r\nexport function addStatLines(left: StatLineValues, right: StatLineValues, sign: 1 | -1 = 1): Record<string, number> {\r\n const sum: Record<string, number> = {};\r\n for (const field of new Set([...Object.keys(left), ...Object.keys(right)])) {\r\n const [a, b] = [left[field], right[field]];\r\n if ((a !== undefined && typeof a !== 'number') || (b !== undefined && typeof b !== 'number')) continue;\r\n sum[field] = (a ?? 0) + sign * (b ?? 0);\r\n }\r\n return sum;\r\n}\r\n\r\n/**\r\n * The contribution of one completed game under a sport's mapping. Every line derives from the\r\n * replayed lines and the stored decision only, never from a stored total (SPORT-202, SPORT-204):\r\n *\r\n * - each team with an opponent: one game, a win, loss, or tie decided from the two replayed scores,\r\n * its points for and against, and the spec's player counters summed from the lines keyed to it;\r\n * - each roster entry of such a team — and any line keyed to one, rostered or not, so no recorded\r\n * stat is lost — its row: games played per the stored decision, and the sport's counters, credited\r\n * to the team in its key;\r\n * - each person who is not a ghost: the sum of their rows.\r\n *\r\n * A row or person line that sums to nothing is left out.\r\n */\r\nexport function buildGameContribution(spec: ContributionSpec, game: FinalizedGame): GameContribution {\r\n const teamStats = game.stats.teamStats as Readonly<Record<string, StatLineValues>>;\r\n const playerStats = game.stats.playerStats as Readonly<Record<string, StatLineValues>>;\r\n const opponentOf = (teamId: string): string | undefined => game.teamIds.find((other) => other !== teamId);\r\n const counted = new Set(game.teamIds.filter((teamId) => opponentOf(teamId) !== undefined));\r\n\r\n const teamLines: Record<string, StatLineValues> = {};\r\n for (const teamId of counted) {\r\n const scored = counter(teamStats[teamId], 'score');\r\n const allowed = counter(teamStats[opponentOf(teamId) as string], 'score');\r\n const line: Record<string, number> = {\r\n gamesPlayed: 1,\r\n wins: scored > allowed ? 1 : 0,\r\n losses: scored < allowed ? 1 : 0,\r\n ties: scored === allowed ? 1 : 0,\r\n pointsFor: scored,\r\n pointsAgainst: allowed,\r\n };\r\n for (const field of spec.teamSeasonPlayerSums) line[field] = 0;\r\n for (const [entryId, playerLine] of Object.entries(playerStats)) {\r\n if (parseGameRosterPlayerId(entryId)?.teamId !== teamId) continue;\r\n for (const field of spec.teamSeasonPlayerSums) line[field] = (line[field] ?? 0) + counter(playerLine, field);\r\n }\r\n teamLines[teamId] = spec.deriveLine('teamSeason', line);\r\n }\r\n\r\n const membershipRows: Record<string, StatLineValues> = {};\r\n const personTotals = new Map<string, Record<string, number>>();\r\n const entryIds = [...new Set([...game.rosterPlayerIds, ...Object.keys(playerStats)])].sort();\r\n for (const entryId of entryIds) {\r\n const entry = parseGameRosterPlayerId(entryId);\r\n if (entry === null || !counted.has(entry.teamId)) continue;\r\n const row: Record<string, number> = { gamesPlayed: isCreditedGamePlayed(game, entryId) ? 1 : 0 };\r\n for (const field of spec.playerStatKeys) row[field] = counter(playerStats[entryId], field);\r\n if (isZeroStatLine(row)) continue;\r\n membershipRows[entryId] = spec.deriveLine('membershipRow', row);\r\n if (isGhostUserId(entry.userId)) continue;\r\n personTotals.set(entry.userId, addStatLines(personTotals.get(entry.userId) ?? {}, row));\r\n }\r\n\r\n const personLines: Record<string, StatLineValues> = {};\r\n for (const [userId, line] of personTotals) personLines[userId] = spec.deriveLine('lifetime', line);\r\n return { teamLines, membershipRows, personLines };\r\n}\r\n\r\n/** A contribution and the sign it is applied with: +1 to add a game, −1 to take it back. */\r\nexport interface SignedContribution {\r\n readonly contribution: GameContribution;\r\n readonly sign: 1 | -1;\r\n}\r\n\r\n/**\r\n * Several signed contributions folded into one, every line through the sport's `deriveLine` and a\r\n * line that sums to nothing left out — the week view's rows, a rebuild's totals, and a ghost link's\r\n * reversal of the old entries beside the merged one, all through the same arithmetic.\r\n */\r\nexport function sumGameContributions(\r\n deriveLine: ContributionSpec['deriveLine'],\r\n signed: readonly SignedContribution[],\r\n): GameContribution {\r\n const fold = (pick: (contribution: GameContribution) => Readonly<Record<string, StatLineValues>>, kind: StatLineKind) => {\r\n const totals = new Map<string, Record<string, number>>();\r\n for (const { contribution, sign } of signed) {\r\n for (const [key, line] of Object.entries(pick(contribution))) {\r\n totals.set(key, addStatLines(totals.get(key) ?? {}, line, sign));\r\n }\r\n }\r\n const lines: Record<string, StatLineValues> = {};\r\n for (const [key, line] of totals) {\r\n const derived = deriveLine(kind, line);\r\n if (!isZeroStatLine(derived)) lines[key] = derived;\r\n }\r\n return lines;\r\n };\r\n return {\r\n teamLines: fold((contribution) => contribution.teamLines, 'teamSeason'),\r\n membershipRows: fold((contribution) => contribution.membershipRows, 'membershipRow'),\r\n personLines: fold((contribution) => contribution.personLines, 'lifetime'),\r\n };\r\n}\r\n\r\n/**\r\n * A stored line moved by a delta: the line after, `null` when it sums to nothing, or the field that\r\n * would go below zero.\r\n */\r\nexport type StatLineChange =\r\n | { readonly ok: true; readonly after: StatLineValues | null }\r\n | { readonly ok: false; readonly field: string };\r\n\r\n/**\r\n * `before` (absent read as no line) moved by `delta` times `sign`, through the sport's `deriveLine`\r\n * for `kind`. A counter that would go below zero is refused, never clamped: it means the stored line\r\n * no longer holds what the games put there.\r\n */\r\nexport function applyStatLineDelta(\r\n deriveLine: ContributionSpec['deriveLine'],\r\n kind: StatLineKind,\r\n before: StatLineValues | undefined,\r\n delta: StatLineValues,\r\n sign: 1 | -1,\r\n): StatLineChange {\r\n // A derived field is recomputed from the moved counters, never moved itself, so a stale stored FP\r\n // can neither go below zero nor survive.\r\n const after = deriveLine(kind, addStatLines(before ?? {}, delta, sign));\r\n const below = Object.entries(after).find(([, value]) => typeof value === 'number' && value < 0);\r\n if (below) return { ok: false, field: below[0] };\r\n return { ok: true, after: isZeroStatLine(after) ? null : after };\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"contribution.js","sourceRoot":"","sources":["../../src/sports/contribution.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gGAAgG;AAChG,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,kGAAkG;AAClG,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,sGAAsG;AACtG,uDAAuD;AAGvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkC7C,MAAM,CAAC,MAAM,uBAAuB,GAAqB,EAAE,SAAS,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;AAiBhH,oEAAoE;AACpE,SAAS,OAAO,CAAC,IAAgC,EAAE,KAAa;IAC9D,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,IAAoB;IACjD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC;AACxF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAuE,EACvE,KAAgB,EAChB,eAAkC,EAClC,qBAAwC;IAExC,IAAI,CAAC,IAAI,CAAC,oBAAoB;QAAE,OAAO,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,KAAK,CAAC,WAAuD,CAAC;IAClF,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE;QACxC,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,IAAI,KAAK,SAAS,IAAI,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,oBAAoB,CAAC,IAAgF,EAAE,OAAe;IACpI,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC5G,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,YAAY,CAAC,IAAoB,EAAE,KAAqB,EAAE,OAAe,CAAC;IACxF,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,IAAI,CAAC,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC;YAAE,SAAS;QACvG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAsB,EAAE,IAAmB;IAC/E,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,SAAqD,CAAC;IACnF,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,WAAuD,CAAC;IACvF,MAAM,UAAU,GAAG,CAAC,MAAc,EAAsB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;IAC1G,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;IAE3F,MAAM,SAAS,GAAmC,EAAE,CAAC;IACrD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAC1E,MAAM,IAAI,GAA2B;YACnC,WAAW,EAAE,CAAC;YACd,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9B,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChC,SAAS,EAAE,MAAM;YACjB,aAAa,EAAE,OAAO;SACvB,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,oBAAoB;YAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/D,KAAK,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAChE,IAAI,uBAAuB,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM;gBAAE,SAAS;YAClE,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,oBAAoB;gBAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC/G,CAAC;QACD,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,cAAc,GAAmC,EAAE,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkC,CAAC;IAC/D,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,eAAe,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,SAAS;QAC3D,MAAM,GAAG,GAA2B,EAAE,WAAW,EAAE,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc;YAAE,GAAG,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;QAC3F,IAAI,cAAc,CAAC,GAAG,CAAC;YAAE,SAAS;QAClC,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;QAChE,IAAI,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,SAAS;QAC1C,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1F,CAAC;IAED,MAAM,WAAW,GAAmC,EAAE,CAAC;IACvD,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,YAAY;QAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACnG,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC;AACpD,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAA0C,EAC1C,MAAqC;IAErC,MAAM,IAAI,GAAG,CAAC,IAAkF,EAAE,IAAkB,EAAE,EAAE;QACtH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkC,CAAC;QACzD,KAAK,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;YAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;gBAC7D,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAmC,EAAE,CAAC;QACjD,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QACrD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IACF,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,YAAY,CAAC;QACvE,cAAc,EAAE,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,eAAe,CAAC;QACpF,WAAW,EAAE,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC;KAC1E,CAAC;AACJ,CAAC;AAUD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAA0C,EAC1C,IAAkB,EAClB,MAAkC,EAClC,KAAqB,EACrB,IAAY;IAEZ,kGAAkG;IAClG,yCAAyC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IAChG,IAAI,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AACnE,CAAC","sourcesContent":["// The aggregate half of the sport adapter contract: who a completed game credits a game played, and\n// what the game adds to every derived destination (SPORT-201, SPORT-204, SPORT-207). Each sport\n// declares its mapping once, as a `ContributionSpec`; the two members built from it — `participation`\n// and `contribution` — are the only place a game's effect on standings, per-membership rows, lifetime\n// lines, and the hall of fame is decided. Finalize, reopen, a ghost link, every rebuild, validation,\n// and the week view all derive through them, so no two paths can disagree about what a game adds.\n//\n// Everything here is pure: no Firestore operation, no clock, no id minted (ARCH-202). The aggregate\n// writer in Functions applies a contribution; the browser sums contributions for the week view.\n//\n// A stored derived line that sums to nothing — every counter and games played zero — is the same as\n// no line at all, as an all-zero player line is in a game's rollup: a contribution leaves one out,\n// and applying a contribution removes a line it returns to zero. That is what makes finalize followed\n// by reopen leave every destination exactly as it was.\n\nimport type { GameStats } from '../doc-schemas/sport-game.js';\nimport { parseGameRosterPlayerId } from '../ids/compound-ids.js';\nimport { isGhostUserId } from '../ids/minted-ids.js';\nimport type { LeagueType } from '../types/sports.js';\nimport type { StatLineKind } from '../types/stats.js';\nimport { isAllZeroLine } from './scoring.js';\nimport type { StatLineValues } from './scoring.js';\n\n/**\n * A completed game as its finalized record holds it: the record's events replayed to lines, and the\n * first page's sport, teams, and participation snapshot. Finalize builds it from the game it is\n * finalizing; every later path builds it from the record, never from the game document (SPORT-204).\n */\nexport interface FinalizedGame {\n readonly leagueType: LeagueType;\n readonly teamIds: readonly string[];\n readonly stats: GameStats;\n readonly rosterPlayerIds: readonly string[];\n readonly absentRosterPlayerIds: readonly string[];\n readonly nonScoringRosterPlayerIds: readonly string[];\n}\n\n/**\n * What one completed game adds to each destination, before the caller's sign. Every line carries its\n * derived fields through the sport's `deriveLine`.\n */\nexport interface GameContribution {\n /** Per team id: the team's standings line (`teamSeason`). A team with no opponent has none. */\n readonly teamLines: Readonly<Record<string, StatLineValues>>;\n /** Per roster-entry id: that membership's row (`membershipRow`), credited to the team in its key. */\n readonly membershipRows: Readonly<Record<string, StatLineValues>>;\n /**\n * Per person who is not a ghost: the sum of that person's rows, games played included — what the\n * game adds to `lifetimeStats.{sportKey}` and, where the sport keeps one, the hall of fame\n * (`lifetime`). A person on both teams of the game counts both sides (SPORT-208 keeps ghosts out).\n */\n readonly personLines: Readonly<Record<string, StatLineValues>>;\n}\n\nexport const EMPTY_GAME_CONTRIBUTION: GameContribution = { teamLines: {}, membershipRows: {}, personLines: {} };\n\n/** One sport's aggregate mapping, declared once by its adapter (SPORT-207). */\nexport interface ContributionSpec {\n /** The counters of a player line that a membership row, a lifetime line, and the hall of fame sum. */\n readonly playerStatKeys: readonly string[];\n /** The player counters summed onto the team's standings line from the lines keyed to that team. */\n readonly teamSeasonPlayerSums: readonly string[];\n /**\n * Whether a present entry whose line holds nothing is credited no game played. Basketball's rule:\n * zero points, rebounds, and assists — zero points alone is not exclusion. Without it only absence\n * withholds a game played.\n */\n readonly zeroLineIsNonScoring: boolean;\n readonly deriveLine: (kind: StatLineKind, line: StatLineValues) => StatLineValues;\n}\n\n/** The value of a counter on a stored line, absent read as zero. */\nfunction counter(line: StatLineValues | undefined, field: string): number {\n const value = line?.[field];\n return typeof value === 'number' ? value : 0;\n}\n\n/** Whether every number a line holds is zero: a derived line that sums to nothing. */\nexport function isZeroStatLine(line: StatLineValues): boolean {\n return Object.values(line).every((value) => typeof value !== 'number' || value === 0);\n}\n\n/**\n * The games-played decision for every roster entry: the entries credited no game played. An absent\n * entry is never credited; under `zeroLineIsNonScoring` a present entry whose line holds nothing is\n * not either. Stored on the completed game as `nonScoringRosterPlayerIds` and read, never decided\n * again, by every later path (SPORT-204). Returned in roster order.\n */\nexport function decideParticipation(\n spec: Pick<ContributionSpec, 'playerStatKeys' | 'zeroLineIsNonScoring'>,\n stats: GameStats,\n rosterPlayerIds: readonly string[],\n absentRosterPlayerIds: readonly string[],\n): string[] {\n if (!spec.zeroLineIsNonScoring) return [];\n const absent = new Set(absentRosterPlayerIds);\n const playerStats = stats.playerStats as Readonly<Record<string, StatLineValues>>;\n return rosterPlayerIds.filter((entryId) => {\n if (absent.has(entryId)) return true;\n const line = playerStats[entryId];\n return line === undefined || isAllZeroLine(line, spec.playerStatKeys);\n });\n}\n\n/** Whether the stored decision credits the entry a game played. */\nexport function isCreditedGamePlayed(game: Pick<FinalizedGame, 'absentRosterPlayerIds' | 'nonScoringRosterPlayerIds'>, entryId: string): boolean {\n return !game.absentRosterPlayerIds.includes(entryId) && !game.nonScoringRosterPlayerIds.includes(entryId);\n}\n\n/** `left` with every number of `right` added `sign` times; a field only one holds is kept. */\nexport function addStatLines(left: StatLineValues, right: StatLineValues, sign: 1 | -1 = 1): Record<string, number> {\n const sum: Record<string, number> = {};\n for (const field of new Set([...Object.keys(left), ...Object.keys(right)])) {\n const [a, b] = [left[field], right[field]];\n if ((a !== undefined && typeof a !== 'number') || (b !== undefined && typeof b !== 'number')) continue;\n sum[field] = (a ?? 0) + sign * (b ?? 0);\n }\n return sum;\n}\n\n/**\n * The contribution of one completed game under a sport's mapping. Every line derives from the\n * replayed lines and the stored decision only, never from a stored total (SPORT-202, SPORT-204):\n *\n * - each team with an opponent: one game, a win, loss, or tie decided from the two replayed scores,\n * its points for and against, and the spec's player counters summed from the lines keyed to it;\n * - each roster entry of such a team — and any line keyed to one, rostered or not, so no recorded\n * stat is lost — its row: games played per the stored decision, and the sport's counters, credited\n * to the team in its key;\n * - each person who is not a ghost: the sum of their rows.\n *\n * A row or person line that sums to nothing is left out.\n */\nexport function buildGameContribution(spec: ContributionSpec, game: FinalizedGame): GameContribution {\n const teamStats = game.stats.teamStats as Readonly<Record<string, StatLineValues>>;\n const playerStats = game.stats.playerStats as Readonly<Record<string, StatLineValues>>;\n const opponentOf = (teamId: string): string | undefined => game.teamIds.find((other) => other !== teamId);\n const counted = new Set(game.teamIds.filter((teamId) => opponentOf(teamId) !== undefined));\n\n const teamLines: Record<string, StatLineValues> = {};\n for (const teamId of counted) {\n const scored = counter(teamStats[teamId], 'score');\n const allowed = counter(teamStats[opponentOf(teamId) as string], 'score');\n const line: Record<string, number> = {\n gamesPlayed: 1,\n wins: scored > allowed ? 1 : 0,\n losses: scored < allowed ? 1 : 0,\n ties: scored === allowed ? 1 : 0,\n pointsFor: scored,\n pointsAgainst: allowed,\n };\n for (const field of spec.teamSeasonPlayerSums) line[field] = 0;\n for (const [entryId, playerLine] of Object.entries(playerStats)) {\n if (parseGameRosterPlayerId(entryId)?.teamId !== teamId) continue;\n for (const field of spec.teamSeasonPlayerSums) line[field] = (line[field] ?? 0) + counter(playerLine, field);\n }\n teamLines[teamId] = spec.deriveLine('teamSeason', line);\n }\n\n const membershipRows: Record<string, StatLineValues> = {};\n const personTotals = new Map<string, Record<string, number>>();\n const entryIds = [...new Set([...game.rosterPlayerIds, ...Object.keys(playerStats)])].sort();\n for (const entryId of entryIds) {\n const entry = parseGameRosterPlayerId(entryId);\n if (entry === null || !counted.has(entry.teamId)) continue;\n const row: Record<string, number> = { gamesPlayed: isCreditedGamePlayed(game, entryId) ? 1 : 0 };\n for (const field of spec.playerStatKeys) row[field] = counter(playerStats[entryId], field);\n if (isZeroStatLine(row)) continue;\n membershipRows[entryId] = spec.deriveLine('membershipRow', row);\n if (isGhostUserId(entry.userId)) continue;\n personTotals.set(entry.userId, addStatLines(personTotals.get(entry.userId) ?? {}, row));\n }\n\n const personLines: Record<string, StatLineValues> = {};\n for (const [userId, line] of personTotals) personLines[userId] = spec.deriveLine('lifetime', line);\n return { teamLines, membershipRows, personLines };\n}\n\n/** A contribution and the sign it is applied with: +1 to add a game, −1 to take it back. */\nexport interface SignedContribution {\n readonly contribution: GameContribution;\n readonly sign: 1 | -1;\n}\n\n/**\n * Several signed contributions folded into one, every line through the sport's `deriveLine` and a\n * line that sums to nothing left out — the week view's rows, a rebuild's totals, and a ghost link's\n * reversal of the old entries beside the merged one, all through the same arithmetic.\n */\nexport function sumGameContributions(\n deriveLine: ContributionSpec['deriveLine'],\n signed: readonly SignedContribution[],\n): GameContribution {\n const fold = (pick: (contribution: GameContribution) => Readonly<Record<string, StatLineValues>>, kind: StatLineKind) => {\n const totals = new Map<string, Record<string, number>>();\n for (const { contribution, sign } of signed) {\n for (const [key, line] of Object.entries(pick(contribution))) {\n totals.set(key, addStatLines(totals.get(key) ?? {}, line, sign));\n }\n }\n const lines: Record<string, StatLineValues> = {};\n for (const [key, line] of totals) {\n const derived = deriveLine(kind, line);\n if (!isZeroStatLine(derived)) lines[key] = derived;\n }\n return lines;\n };\n return {\n teamLines: fold((contribution) => contribution.teamLines, 'teamSeason'),\n membershipRows: fold((contribution) => contribution.membershipRows, 'membershipRow'),\n personLines: fold((contribution) => contribution.personLines, 'lifetime'),\n };\n}\n\n/**\n * A stored line moved by a delta: the line after, `null` when it sums to nothing, or the field that\n * would go below zero.\n */\nexport type StatLineChange =\n | { readonly ok: true; readonly after: StatLineValues | null }\n | { readonly ok: false; readonly field: string };\n\n/**\n * `before` (absent read as no line) moved by `delta` times `sign`, through the sport's `deriveLine`\n * for `kind`. A counter that would go below zero is refused, never clamped: it means the stored line\n * no longer holds what the games put there.\n */\nexport function applyStatLineDelta(\n deriveLine: ContributionSpec['deriveLine'],\n kind: StatLineKind,\n before: StatLineValues | undefined,\n delta: StatLineValues,\n sign: 1 | -1,\n): StatLineChange {\n // A derived field is recomputed from the moved counters, never moved itself, so a stale stored FP\n // can neither go below zero nor survive.\n const after = deriveLine(kind, addStatLines(before ?? {}, delta, sign));\n const below = Object.entries(after).find(([, value]) => typeof value === 'number' && value < 0);\n if (below) return { ok: false, field: below[0] };\n return { ok: true, after: isZeroStatLine(after) ? null : after };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"curling-scoring.js","sourceRoot":"","sources":["../../src/sports/curling-scoring.ts"],"names":[],"mappings":"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","sourcesContent":["// The curling adapter's scoring, display, and bulk-entry members. Curling is team-scored by round: a\r\n// round event records one round — one team's points, or a blank round for every team — and nothing\r\n// names a player. A round is recorded once, in order: `validate` accepts a round event only for the\r\n// first unrecorded round, and re-scoring a round is a void of its event plus a new one, so no curling\r\n// event is ever superseded and undoing one always returns its round to unrecorded (SPORT-103).\r\n\r\nimport { z } from 'zod';\r\nimport { CURLING_ROUND_COUNT, MAX_CURLING_ROUND_POINTS, MAX_STAT_EVENTS_PER_WRITE } from '../constants/sports.js';\r\nimport type { CurlingTeamStats, GameStatEvent } from '../doc-schemas/game-stats.js';\r\nimport type { CurlingGameStats, GameStats } from '../doc-schemas/sport-game.js';\r\nimport {\r\n CURLING_ROUND_BLANK_EVENT_TYPE,\r\n CURLING_ROUND_SCORE_EVENT_TYPE,\r\n CURLING_STAT_EVENT_TYPE_KEYS,\r\n} from '../types/sports.js';\r\nimport type { CurlingStatEventType, CurlingTeamStatKey } from '../types/sports.js';\r\nimport { STAT_VOID_EVENT_TYPE } from '../types/stats.js';\r\nimport {\r\n BULK_ENTRY_TOO_LARGE_MESSAGE,\r\n bulkEntryCountField,\r\n type BulkEntryContext,\r\n type RoundsBulkEntry,\r\n type RoundsValues,\r\n type StatEventDraft,\r\n} from './bulk-entry.js';\r\nimport { buildGameContribution, decideParticipation, type ContributionSpec } from './contribution.js';\r\nimport {\r\n FINALIZATION_READY,\r\n STAT_EVENT_ACCEPTED,\r\n isSportStatEventIn,\r\n isStatCorrection,\r\n refuseStatEvent,\r\n type CorrectableFields,\r\n type FinalizationReadiness,\r\n type GameTeamLine,\r\n type SportScoring,\r\n type SportStatEventBody,\r\n type StatEventBody,\r\n} from './scoring.js';\r\nimport {\r\n describeStoreEvent,\r\n type RoundsDisplay,\r\n type RoundsScoringPanel,\r\n type Scoreboard,\r\n type StatEventLabel,\r\n type StatEventNameResolver,\r\n type StatFieldLabels,\r\n} from './scoring-display.js';\r\n\r\ntype CurlingEvent = Extract<SportStatEventBody, { type: CurlingStatEventType }>;\r\n\r\nfunction isCurlingEvent(event: StatEventBody | undefined): event is CurlingEvent {\r\n return isSportStatEventIn(CURLING_STAT_EVENT_TYPE_KEYS, event);\r\n}\r\n\r\n// The registry chose this adapter by the game's `leagueType`, so the rollup it is handed is a\r\n// curling game's (SPORT-002).\r\nfunction ownStats(state: GameStats): CurlingGameStats {\r\n return state as CurlingGameStats;\r\n}\r\n\r\nfunction ownTeamLine(line: GameTeamLine): CurlingTeamStats {\r\n return line as CurlingTeamStats;\r\n}\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Lines — a team's `score` is always the sum of its recorded rounds\r\n// ---------------------------------------------------------------------------------------------\r\n\r\ntype RoundSlots = CurlingTeamStats['roundScores'];\r\n\r\nfunction sumOfRounds(roundScores: RoundSlots): number {\r\n return roundScores.reduce<number>((total, slot) => total + (slot ?? 0), 0);\r\n}\r\n\r\n/** Every round unrecorded — `null`, where a blank round is `0`. */\r\nexport function emptyCurlingTeamLine(): CurlingTeamStats {\r\n const roundScores: RoundSlots = Array.from({ length: CURLING_ROUND_COUNT }, () => null);\r\n return { score: sumOfRounds(roundScores), roundScores };\r\n}\r\n\r\nfunction withRound(line: CurlingTeamStats, roundNumber: number, slot: number | null): CurlingTeamStats {\r\n const roundScores = [...line.roundScores];\r\n roundScores[roundNumber - 1] = slot;\r\n return { score: sumOfRounds(roundScores), roundScores };\r\n}\r\n\r\n/**\r\n * Record or clear one round on every team line: a scored round is the scoring team's points and 0\r\n * for the others, a blank round 0 for all, and a cleared round `null` for all.\r\n */\r\nfunction setRound(stateIn: GameStats, event: CurlingEvent, recorded: boolean): CurlingGameStats {\r\n const state = ownStats(stateIn);\r\n const teamStats = { ...state.teamStats };\r\n const scoringTeamId = event.type === CURLING_ROUND_SCORE_EVENT_TYPE ? event.teamId : undefined;\r\n if (scoringTeamId !== undefined && teamStats[scoringTeamId] === undefined) {\r\n teamStats[scoringTeamId] = emptyCurlingTeamLine();\r\n }\r\n for (const [teamId, line] of Object.entries(teamStats)) {\r\n const points = event.type === CURLING_ROUND_SCORE_EVENT_TYPE && teamId === scoringTeamId ? event.points : 0;\r\n teamStats[teamId] = withRound(line, event.roundNumber, recorded ? points : null);\r\n }\r\n return { teamStats, playerStats: state.playerStats };\r\n}\r\n\r\nfunction nextCurlingRound(stateIn: GameStats): number | null {\r\n const lines = Object.values(ownStats(stateIn).teamStats);\r\n for (let roundNumber = 1; roundNumber <= CURLING_ROUND_COUNT; roundNumber += 1) {\r\n if (lines.length === 0 || lines.some((line) => line.roundScores[roundNumber - 1] === null)) return roundNumber;\r\n }\r\n return null;\r\n}\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Scoring\r\n// ---------------------------------------------------------------------------------------------\r\n\r\n// No curling field is correctable: a curling result changes only by reopening the game and\r\n// re-scoring the round.\r\nconst CORRECTABLE_FIELDS: CorrectableFields = { team: [], player: [] };\r\n\r\nfunction namesPlayer(event: StatEventBody): boolean {\r\n return 'playerUserId' in event && event.playerUserId !== undefined;\r\n}\r\n\r\nconst validate: SportScoring['validate'] = (state, event, roster) => {\r\n if (!isCurlingEvent(event)) return refuseStatEvent('eventTypeNotInSport');\r\n if (namesPlayer(event)) return refuseStatEvent('playerNotAllowed');\r\n if (event.type === CURLING_ROUND_SCORE_EVENT_TYPE && !roster.teamIds.includes(event.teamId)) {\r\n return refuseStatEvent('teamNotInGame');\r\n }\r\n const next = nextCurlingRound(state);\r\n if (next === null) return refuseStatEvent('roundsComplete');\r\n return event.roundNumber === next ? STAT_EVENT_ACCEPTED : refuseStatEvent('roundNotNext');\r\n};\r\n\r\nfunction finalizationReadiness(state: GameStats): FinalizationReadiness {\r\n return nextCurlingRound(state) === null ? FINALIZATION_READY : { ready: false, reason: 'roundsIncomplete' };\r\n}\r\n\r\n// What a completed curling game adds: the standings line from the two scores, and a game played\r\n// to every present roster entry, whatever its team scored. There are no individual stats, so only\r\n// absence withholds a game played and no entry is ever non-scoring.\r\nconst CURLING_CONTRIBUTION: ContributionSpec = {\r\n playerStatKeys: [],\r\n teamSeasonPlayerSums: [],\r\n zeroLineIsNonScoring: false,\r\n deriveLine: (_kind, line) => line,\r\n};\r\n\r\nexport const CURLING_SCORING: SportScoring = {\r\n validate,\r\n apply: (state, event) => {\r\n if (isStatCorrection(event)) throw new Error('No curling field is correctable.');\r\n if (!isCurlingEvent(event)) throw new Error(`The curling adapter does not apply ${event.type}.`);\r\n return setRound(state, event, true);\r\n },\r\n undo: (state, target) => {\r\n if (!isCurlingEvent(target)) throw new Error(`The curling adapter does not undo ${target.type}.`);\r\n return setRound(state, target, false);\r\n },\r\n // A curling line has no derived field.\r\n deriveLine: CURLING_CONTRIBUTION.deriveLine,\r\n correctableFields: CORRECTABLE_FIELDS,\r\n nextRound: nextCurlingRound,\r\n finalizationReadiness,\r\n participation: (state, rosterPlayerIds, absentRosterPlayerIds) =>\r\n decideParticipation(CURLING_CONTRIBUTION, state, rosterPlayerIds, absentRosterPlayerIds),\r\n contribution: (game) => buildGameContribution(CURLING_CONTRIBUTION, game),\r\n};\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Display\r\n// ---------------------------------------------------------------------------------------------\r\n\r\nexport const CURLING_ROUNDS: RoundsDisplay = { count: CURLING_ROUND_COUNT, label: 'Round' };\r\n\r\nexport const CURLING_EVENT_LABELS = {\r\n [CURLING_ROUND_SCORE_EVENT_TYPE]: { button: 'Score', play: 'Round score' },\r\n [CURLING_ROUND_BLANK_EVENT_TYPE]: { button: '+0 (Blank)', play: 'Blank end' },\r\n} as const satisfies Record<CurlingStatEventType, StatEventLabel>;\r\n\r\nexport const CURLING_FIELD_LABELS = {\r\n team: { score: 'Score' },\r\n player: {},\r\n} as const satisfies StatFieldLabels<CurlingTeamStatKey, never>;\r\n\r\nexport function describeCurlingEvent(event: GameStatEvent, resolve: StatEventNameResolver): string {\r\n const storeEvent = describeStoreEvent(event, resolve, CURLING_FIELD_LABELS);\r\n if (storeEvent !== null) return storeEvent;\r\n if (!isCurlingEvent(event)) return event.type;\r\n const round = `${CURLING_ROUNDS.label} ${event.roundNumber}`;\r\n if (event.type === CURLING_ROUND_BLANK_EVENT_TYPE) return `${round}: ${CURLING_EVENT_LABELS[event.type].play.toLowerCase()}`;\r\n return `${round}: ${resolve.team(event.teamId)} scored ${event.points}`;\r\n}\r\n\r\nexport const CURLING_SCOREBOARD: Scoreboard = {\r\n figures: [{ id: 'points', label: 'Points', value: (teamLine) => teamLine.score }],\r\n jumpBall: false,\r\n roundSlots: (teamLine) => ownTeamLine(teamLine).roundScores,\r\n};\r\n\r\nexport const CURLING_SCORING_PANEL: RoundsScoringPanel = {\r\n kind: 'rounds',\r\n scoreEventType: CURLING_ROUND_SCORE_EVENT_TYPE,\r\n blankEventType: CURLING_ROUND_BLANK_EVENT_TYPE,\r\n maxPoints: MAX_CURLING_ROUND_POINTS,\r\n};\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Bulk entry — one row per round, a cell per team\r\n// ---------------------------------------------------------------------------------------------\r\n\r\nexport const CURLING_ROUND_POINTS_MESSAGE = `A round is worth 0 to ${MAX_CURLING_ROUND_POINTS} points.`;\r\nexport const CURLING_ONE_SCORING_TEAM_MESSAGE =\r\n 'A round is scored by one team (the other 0 or empty), blank (0 for every team), or left empty.';\r\nexport const CURLING_NO_SKIPPED_ROUND_MESSAGE = 'Rounds are recorded in order: fill in every earlier round first.';\r\n\r\ntype RoundOutcome =\r\n | { readonly kind: 'unrecorded' }\r\n | { readonly kind: 'blank' }\r\n | { readonly kind: 'scored'; readonly teamId: string; readonly points: number }\r\n | { readonly kind: 'invalid' };\r\n\r\nfunction roundOutcome(row: Readonly<Record<string, number | null | undefined>>, teamIds: readonly string[]): RoundOutcome {\r\n const cells = teamIds.map((teamId) => ({ teamId, points: row[teamId] ?? null }));\r\n if (cells.every((cell) => cell.points === null)) return { kind: 'unrecorded' };\r\n if (cells.every((cell) => cell.points === 0)) return { kind: 'blank' };\r\n const scorers = cells.filter((cell) => cell.points !== null && cell.points > 0);\r\n const [scorer] = scorers;\r\n if (scorers.length === 1 && scorer !== undefined && scorer.points !== null) {\r\n return { kind: 'scored', teamId: scorer.teamId, points: scorer.points };\r\n }\r\n return { kind: 'invalid' };\r\n}\r\n\r\nfunction sameOutcome(left: RoundOutcome, right: RoundOutcome): boolean {\r\n if (left.kind === 'scored' && right.kind === 'scored') return left.teamId === right.teamId && left.points === right.points;\r\n return left.kind === right.kind;\r\n}\r\n\r\nfunction storedRound(context: BulkEntryContext, roundNumber: number): Record<string, number | null> {\r\n const state = ownStats(context.state);\r\n return Object.fromEntries(\r\n context.game.teamIds.map((teamId) => [teamId, state.teamStats[teamId]?.roundScores[roundNumber - 1] ?? null]),\r\n );\r\n}\r\n\r\nfunction roundDrafts(values: RoundsValues, context: BulkEntryContext): StatEventDraft[] {\r\n const { teamIds } = context.game;\r\n const liveEventByRound = new Map(context.liveRoundEvents.map((event) => [event.roundNumber, event.eventId]));\r\n const voids: StatEventDraft[] = [];\r\n const recorded: StatEventDraft[] = [];\r\n values.rounds.forEach((row, index) => {\r\n const roundNumber = index + 1;\r\n const entered = roundOutcome(row, teamIds);\r\n if (sameOutcome(entered, roundOutcome(storedRound(context, roundNumber), teamIds))) return;\r\n const liveEventId = liveEventByRound.get(roundNumber);\r\n if (liveEventId !== undefined) voids.push({ type: STAT_VOID_EVENT_TYPE, targetEventId: liveEventId });\r\n if (entered.kind === 'scored') {\r\n recorded.push({ type: CURLING_ROUND_SCORE_EVENT_TYPE, teamId: entered.teamId, roundNumber, points: entered.points });\r\n } else if (entered.kind === 'blank') {\r\n recorded.push({ type: CURLING_ROUND_BLANK_EVENT_TYPE, roundNumber });\r\n }\r\n });\r\n // Every void first, then the new rounds in order: each changed round is unrecorded again before\r\n // any new event is validated, so each new event is the next unrecorded round when it arrives.\r\n return [...voids, ...recorded];\r\n}\r\n\r\nconst roundCell = bulkEntryCountField\r\n .pipe(z.number().max(MAX_CURLING_ROUND_POINTS, CURLING_ROUND_POINTS_MESSAGE))\r\n .nullable();\r\n\r\nexport const CURLING_BULK_ENTRY: RoundsBulkEntry = {\r\n kind: 'rounds',\r\n formDefaults: (context) => ({\r\n rounds: Array.from({ length: CURLING_ROUND_COUNT }, (_, index) => storedRound(context, index + 1)),\r\n }),\r\n formSchema: (context) => {\r\n const { teamIds } = context.game;\r\n const row = z.strictObject(Object.fromEntries(teamIds.map((teamId) => [teamId, roundCell])));\r\n return z\r\n .object({ rounds: z.array(row).length(CURLING_ROUND_COUNT) })\r\n .superRefine((values, issues) => {\r\n let unrecordedBefore = false;\r\n values.rounds.forEach((cells, index) => {\r\n const outcome = roundOutcome(cells, teamIds);\r\n if (outcome.kind === 'invalid') {\r\n issues.addIssue({ code: 'custom', path: ['rounds', index], message: CURLING_ONE_SCORING_TEAM_MESSAGE });\r\n } else if (outcome.kind === 'unrecorded') {\r\n unrecordedBefore = true;\r\n } else if (unrecordedBefore) {\r\n issues.addIssue({ code: 'custom', path: ['rounds', index], message: CURLING_NO_SKIPPED_ROUND_MESSAGE });\r\n }\r\n });\r\n if (roundDrafts(values, context).length > MAX_STAT_EVENTS_PER_WRITE) {\r\n issues.addIssue({ code: 'custom', path: ['rounds'], message: BULK_ENTRY_TOO_LARGE_MESSAGE });\r\n }\r\n });\r\n },\r\n toEvents: roundDrafts,\r\n};\r\n"]}
|
|
1
|
+
{"version":3,"file":"curling-scoring.js","sourceRoot":"","sources":["../../src/sports/curling-scoring.ts"],"names":[],"mappings":"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","sourcesContent":["// The curling adapter's scoring, display, and bulk-entry members. Curling is team-scored by round: a\n// round event records one round — one team's points, or a blank round for every team — and nothing\n// names a player. A round is recorded once, in order: `validate` accepts a round event only for the\n// first unrecorded round, and re-scoring a round is a void of its event plus a new one, so no curling\n// event is ever superseded and undoing one always returns its round to unrecorded (SPORT-103).\n\nimport { z } from 'zod';\nimport { CURLING_ROUND_COUNT, MAX_CURLING_ROUND_POINTS, MAX_STAT_EVENTS_PER_WRITE } from '../constants/sports.js';\nimport type { CurlingTeamStats, GameStatEvent } from '../doc-schemas/game-stats.js';\nimport type { CurlingGameStats, GameStats } from '../doc-schemas/sport-game.js';\nimport {\n CURLING_ROUND_BLANK_EVENT_TYPE,\n CURLING_ROUND_SCORE_EVENT_TYPE,\n CURLING_STAT_EVENT_TYPE_KEYS,\n} from '../types/sports.js';\nimport type { CurlingStatEventType, CurlingTeamStatKey } from '../types/sports.js';\nimport { STAT_VOID_EVENT_TYPE } from '../types/stats.js';\nimport {\n BULK_ENTRY_TOO_LARGE_MESSAGE,\n bulkEntryCountField,\n type BulkEntryContext,\n type RoundsBulkEntry,\n type RoundsValues,\n type StatEventDraft,\n} from './bulk-entry.js';\nimport { buildGameContribution, decideParticipation, type ContributionSpec } from './contribution.js';\nimport {\n FINALIZATION_READY,\n STAT_EVENT_ACCEPTED,\n isSportStatEventIn,\n isStatCorrection,\n refuseStatEvent,\n type CorrectableFields,\n type FinalizationReadiness,\n type GameTeamLine,\n type SportScoring,\n type SportStatEventBody,\n type StatEventBody,\n} from './scoring.js';\nimport {\n describeStoreEvent,\n type RoundsDisplay,\n type RoundsScoringPanel,\n type Scoreboard,\n type StatEventLabel,\n type StatEventNameResolver,\n type StatFieldLabels,\n} from './scoring-display.js';\n\ntype CurlingEvent = Extract<SportStatEventBody, { type: CurlingStatEventType }>;\n\nfunction isCurlingEvent(event: StatEventBody | undefined): event is CurlingEvent {\n return isSportStatEventIn(CURLING_STAT_EVENT_TYPE_KEYS, event);\n}\n\n// The registry chose this adapter by the game's `leagueType`, so the rollup it is handed is a\n// curling game's (SPORT-002).\nfunction ownStats(state: GameStats): CurlingGameStats {\n return state as CurlingGameStats;\n}\n\nfunction ownTeamLine(line: GameTeamLine): CurlingTeamStats {\n return line as CurlingTeamStats;\n}\n\n// ---------------------------------------------------------------------------------------------\n// Lines — a team's `score` is always the sum of its recorded rounds\n// ---------------------------------------------------------------------------------------------\n\ntype RoundSlots = CurlingTeamStats['roundScores'];\n\nfunction sumOfRounds(roundScores: RoundSlots): number {\n return roundScores.reduce<number>((total, slot) => total + (slot ?? 0), 0);\n}\n\n/** Every round unrecorded — `null`, where a blank round is `0`. */\nexport function emptyCurlingTeamLine(): CurlingTeamStats {\n const roundScores: RoundSlots = Array.from({ length: CURLING_ROUND_COUNT }, () => null);\n return { score: sumOfRounds(roundScores), roundScores };\n}\n\nfunction withRound(line: CurlingTeamStats, roundNumber: number, slot: number | null): CurlingTeamStats {\n const roundScores = [...line.roundScores];\n roundScores[roundNumber - 1] = slot;\n return { score: sumOfRounds(roundScores), roundScores };\n}\n\n/**\n * Record or clear one round on every team line: a scored round is the scoring team's points and 0\n * for the others, a blank round 0 for all, and a cleared round `null` for all.\n */\nfunction setRound(stateIn: GameStats, event: CurlingEvent, recorded: boolean): CurlingGameStats {\n const state = ownStats(stateIn);\n const teamStats = { ...state.teamStats };\n const scoringTeamId = event.type === CURLING_ROUND_SCORE_EVENT_TYPE ? event.teamId : undefined;\n if (scoringTeamId !== undefined && teamStats[scoringTeamId] === undefined) {\n teamStats[scoringTeamId] = emptyCurlingTeamLine();\n }\n for (const [teamId, line] of Object.entries(teamStats)) {\n const points = event.type === CURLING_ROUND_SCORE_EVENT_TYPE && teamId === scoringTeamId ? event.points : 0;\n teamStats[teamId] = withRound(line, event.roundNumber, recorded ? points : null);\n }\n return { teamStats, playerStats: state.playerStats };\n}\n\nfunction nextCurlingRound(stateIn: GameStats): number | null {\n const lines = Object.values(ownStats(stateIn).teamStats);\n for (let roundNumber = 1; roundNumber <= CURLING_ROUND_COUNT; roundNumber += 1) {\n if (lines.length === 0 || lines.some((line) => line.roundScores[roundNumber - 1] === null)) return roundNumber;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------------------------\n// Scoring\n// ---------------------------------------------------------------------------------------------\n\n// No curling field is correctable: a curling result changes only by reopening the game and\n// re-scoring the round.\nconst CORRECTABLE_FIELDS: CorrectableFields = { team: [], player: [] };\n\nfunction namesPlayer(event: StatEventBody): boolean {\n return 'playerUserId' in event && event.playerUserId !== undefined;\n}\n\nconst validate: SportScoring['validate'] = (state, event, roster) => {\n if (!isCurlingEvent(event)) return refuseStatEvent('eventTypeNotInSport');\n if (namesPlayer(event)) return refuseStatEvent('playerNotAllowed');\n if (event.type === CURLING_ROUND_SCORE_EVENT_TYPE && !roster.teamIds.includes(event.teamId)) {\n return refuseStatEvent('teamNotInGame');\n }\n const next = nextCurlingRound(state);\n if (next === null) return refuseStatEvent('roundsComplete');\n return event.roundNumber === next ? STAT_EVENT_ACCEPTED : refuseStatEvent('roundNotNext');\n};\n\nfunction finalizationReadiness(state: GameStats): FinalizationReadiness {\n return nextCurlingRound(state) === null ? FINALIZATION_READY : { ready: false, reason: 'roundsIncomplete' };\n}\n\n// What a completed curling game adds: the standings line from the two scores, and a game played\n// to every present roster entry, whatever its team scored. There are no individual stats, so only\n// absence withholds a game played and no entry is ever non-scoring.\nconst CURLING_CONTRIBUTION: ContributionSpec = {\n playerStatKeys: [],\n teamSeasonPlayerSums: [],\n zeroLineIsNonScoring: false,\n deriveLine: (_kind, line) => line,\n};\n\nexport const CURLING_SCORING: SportScoring = {\n validate,\n apply: (state, event) => {\n if (isStatCorrection(event)) throw new Error('No curling field is correctable.');\n if (!isCurlingEvent(event)) throw new Error(`The curling adapter does not apply ${event.type}.`);\n return setRound(state, event, true);\n },\n undo: (state, target) => {\n if (!isCurlingEvent(target)) throw new Error(`The curling adapter does not undo ${target.type}.`);\n return setRound(state, target, false);\n },\n // A curling line has no derived field.\n deriveLine: CURLING_CONTRIBUTION.deriveLine,\n correctableFields: CORRECTABLE_FIELDS,\n nextRound: nextCurlingRound,\n finalizationReadiness,\n participation: (state, rosterPlayerIds, absentRosterPlayerIds) =>\n decideParticipation(CURLING_CONTRIBUTION, state, rosterPlayerIds, absentRosterPlayerIds),\n contribution: (game) => buildGameContribution(CURLING_CONTRIBUTION, game),\n};\n\n// ---------------------------------------------------------------------------------------------\n// Display\n// ---------------------------------------------------------------------------------------------\n\nexport const CURLING_ROUNDS: RoundsDisplay = { count: CURLING_ROUND_COUNT, label: 'Round' };\n\nexport const CURLING_EVENT_LABELS = {\n [CURLING_ROUND_SCORE_EVENT_TYPE]: { button: 'Score', play: 'Round score' },\n [CURLING_ROUND_BLANK_EVENT_TYPE]: { button: '+0 (Blank)', play: 'Blank end' },\n} as const satisfies Record<CurlingStatEventType, StatEventLabel>;\n\nexport const CURLING_FIELD_LABELS = {\n team: { score: 'Score' },\n player: {},\n} as const satisfies StatFieldLabels<CurlingTeamStatKey, never>;\n\nexport function describeCurlingEvent(event: GameStatEvent, resolve: StatEventNameResolver): string {\n const storeEvent = describeStoreEvent(event, resolve, CURLING_FIELD_LABELS);\n if (storeEvent !== null) return storeEvent;\n if (!isCurlingEvent(event)) return event.type;\n const round = `${CURLING_ROUNDS.label} ${event.roundNumber}`;\n if (event.type === CURLING_ROUND_BLANK_EVENT_TYPE) return `${round}: ${CURLING_EVENT_LABELS[event.type].play.toLowerCase()}`;\n return `${round}: ${resolve.team(event.teamId)} scored ${event.points}`;\n}\n\nexport const CURLING_SCOREBOARD: Scoreboard = {\n figures: [{ id: 'points', label: 'Points', value: (teamLine) => teamLine.score }],\n jumpBall: false,\n roundSlots: (teamLine) => ownTeamLine(teamLine).roundScores,\n};\n\nexport const CURLING_SCORING_PANEL: RoundsScoringPanel = {\n kind: 'rounds',\n scoreEventType: CURLING_ROUND_SCORE_EVENT_TYPE,\n blankEventType: CURLING_ROUND_BLANK_EVENT_TYPE,\n maxPoints: MAX_CURLING_ROUND_POINTS,\n};\n\n// ---------------------------------------------------------------------------------------------\n// Bulk entry — one row per round, a cell per team\n// ---------------------------------------------------------------------------------------------\n\nexport const CURLING_ROUND_POINTS_MESSAGE = `A round is worth 0 to ${MAX_CURLING_ROUND_POINTS} points.`;\nexport const CURLING_ONE_SCORING_TEAM_MESSAGE =\n 'A round is scored by one team (the other 0 or empty), blank (0 for every team), or left empty.';\nexport const CURLING_NO_SKIPPED_ROUND_MESSAGE = 'Rounds are recorded in order: fill in every earlier round first.';\n\ntype RoundOutcome =\n | { readonly kind: 'unrecorded' }\n | { readonly kind: 'blank' }\n | { readonly kind: 'scored'; readonly teamId: string; readonly points: number }\n | { readonly kind: 'invalid' };\n\nfunction roundOutcome(row: Readonly<Record<string, number | null | undefined>>, teamIds: readonly string[]): RoundOutcome {\n const cells = teamIds.map((teamId) => ({ teamId, points: row[teamId] ?? null }));\n if (cells.every((cell) => cell.points === null)) return { kind: 'unrecorded' };\n if (cells.every((cell) => cell.points === 0)) return { kind: 'blank' };\n const scorers = cells.filter((cell) => cell.points !== null && cell.points > 0);\n const [scorer] = scorers;\n if (scorers.length === 1 && scorer !== undefined && scorer.points !== null) {\n return { kind: 'scored', teamId: scorer.teamId, points: scorer.points };\n }\n return { kind: 'invalid' };\n}\n\nfunction sameOutcome(left: RoundOutcome, right: RoundOutcome): boolean {\n if (left.kind === 'scored' && right.kind === 'scored') return left.teamId === right.teamId && left.points === right.points;\n return left.kind === right.kind;\n}\n\nfunction storedRound(context: BulkEntryContext, roundNumber: number): Record<string, number | null> {\n const state = ownStats(context.state);\n return Object.fromEntries(\n context.game.teamIds.map((teamId) => [teamId, state.teamStats[teamId]?.roundScores[roundNumber - 1] ?? null]),\n );\n}\n\nfunction roundDrafts(values: RoundsValues, context: BulkEntryContext): StatEventDraft[] {\n const { teamIds } = context.game;\n const liveEventByRound = new Map(context.liveRoundEvents.map((event) => [event.roundNumber, event.eventId]));\n const voids: StatEventDraft[] = [];\n const recorded: StatEventDraft[] = [];\n values.rounds.forEach((row, index) => {\n const roundNumber = index + 1;\n const entered = roundOutcome(row, teamIds);\n if (sameOutcome(entered, roundOutcome(storedRound(context, roundNumber), teamIds))) return;\n const liveEventId = liveEventByRound.get(roundNumber);\n if (liveEventId !== undefined) voids.push({ type: STAT_VOID_EVENT_TYPE, targetEventId: liveEventId });\n if (entered.kind === 'scored') {\n recorded.push({ type: CURLING_ROUND_SCORE_EVENT_TYPE, teamId: entered.teamId, roundNumber, points: entered.points });\n } else if (entered.kind === 'blank') {\n recorded.push({ type: CURLING_ROUND_BLANK_EVENT_TYPE, roundNumber });\n }\n });\n // Every void first, then the new rounds in order: each changed round is unrecorded again before\n // any new event is validated, so each new event is the next unrecorded round when it arrives.\n return [...voids, ...recorded];\n}\n\nconst roundCell = bulkEntryCountField\n .pipe(z.number().max(MAX_CURLING_ROUND_POINTS, CURLING_ROUND_POINTS_MESSAGE))\n .nullable();\n\nexport const CURLING_BULK_ENTRY: RoundsBulkEntry = {\n kind: 'rounds',\n formDefaults: (context) => ({\n rounds: Array.from({ length: CURLING_ROUND_COUNT }, (_, index) => storedRound(context, index + 1)),\n }),\n formSchema: (context) => {\n const { teamIds } = context.game;\n const row = z.strictObject(Object.fromEntries(teamIds.map((teamId) => [teamId, roundCell])));\n return z\n .object({ rounds: z.array(row).length(CURLING_ROUND_COUNT) })\n .superRefine((values, issues) => {\n let unrecordedBefore = false;\n values.rounds.forEach((cells, index) => {\n const outcome = roundOutcome(cells, teamIds);\n if (outcome.kind === 'invalid') {\n issues.addIssue({ code: 'custom', path: ['rounds', index], message: CURLING_ONE_SCORING_TEAM_MESSAGE });\n } else if (outcome.kind === 'unrecorded') {\n unrecordedBefore = true;\n } else if (unrecordedBefore) {\n issues.addIssue({ code: 'custom', path: ['rounds', index], message: CURLING_NO_SKIPPED_ROUND_MESSAGE });\n }\n });\n if (roundDrafts(values, context).length > MAX_STAT_EVENTS_PER_WRITE) {\n issues.addIssue({ code: 'custom', path: ['rounds'], message: BULK_ENTRY_TOO_LARGE_MESSAGE });\n }\n });\n },\n toEvents: roundDrafts,\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finalized-record.js","sourceRoot":"","sources":["../../src/sports/finalized-record.ts"],"names":[],"mappings":"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","sourcesContent":["// A completed game's finalized record, `sportGames/{gameId}/gameStatHistory/{pageId}`: every event\
|
|
1
|
+
{"version":3,"file":"finalized-record.js","sourceRoot":"","sources":["../../src/sports/finalized-record.ts"],"names":[],"mappings":"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","sourcesContent":["// A completed game's finalized record, `sportGames/{gameId}/gameStatHistory/{pageId}`: every event\n// document of the game in `sequence` order, in pages of up to STAT_HISTORY_PAGE_SIZE, the first page\n// carrying the sport, the teams, and the participation snapshot finalize applied. These are the one\n// builder and the one reader of its pages, so finalize, the game copy, a ghost link, and the week\n// view all write and read the same record.\n\nimport { STAT_HISTORY_PAGE_SIZE } from '../constants/sports.js';\nimport type { GameStatEvent } from '../doc-schemas/game-stats.js';\nimport type { GameStatHistoryFirstPage, GameStatHistoryPage } from '../doc-schemas/game-stat-history.js';\nimport { buildStatHistoryPageId } from '../ids/stat-event-ids.js';\nimport type { FinalizedGame } from './contribution.js';\nimport { replayGameStats } from './replay.js';\nimport type { AnySportAdapter } from './sport-adapter.js';\n\n/** What a record holds beside its events: the sport, the teams, and the participation snapshot. */\nexport type FinalizedRecordSnapshot = Omit<GameStatHistoryFirstPage, 'events' | 'pageCount'>;\n\n/** One page as written, at its id. */\nexport interface FinalizedRecordPage {\n readonly pageId: string;\n readonly page: GameStatHistoryPage;\n}\n\n/**\n * The record's pages: the events in `sequence` order, cut into pages, the first carrying the page\n * count and the snapshot. A game with no events still has its first page.\n */\nexport function buildFinalizedRecordPages(\n events: readonly GameStatEvent[],\n snapshot: Pick<\n FinalizedGame,\n 'leagueType' | 'teamIds' | 'rosterPlayerIds' | 'absentRosterPlayerIds' | 'nonScoringRosterPlayerIds'\n >,\n): FinalizedRecordPage[] {\n const ordered = [...events].sort((left, right) => left.sequence - right.sequence);\n const pageCount = Math.max(1, Math.ceil(ordered.length / STAT_HISTORY_PAGE_SIZE));\n const pages: FinalizedRecordPage[] = [];\n for (let index = 0; index < pageCount; index += 1) {\n const slice = ordered.slice(index * STAT_HISTORY_PAGE_SIZE, (index + 1) * STAT_HISTORY_PAGE_SIZE);\n const page: GameStatHistoryPage =\n index === 0\n ? {\n events: slice,\n pageCount,\n leagueType: snapshot.leagueType,\n teamIds: [...snapshot.teamIds],\n rosterPlayerIds: [...snapshot.rosterPlayerIds],\n absentRosterPlayerIds: [...snapshot.absentRosterPlayerIds],\n nonScoringRosterPlayerIds: [...snapshot.nonScoringRosterPlayerIds],\n }\n : { events: slice };\n pages.push({ pageId: buildStatHistoryPageId(index), page });\n }\n return pages;\n}\n\n/** Whether a page is a record's first page, which carries the snapshot. */\nexport function isFirstRecordPage(page: GameStatHistoryPage): page is GameStatHistoryFirstPage {\n return 'pageCount' in page;\n}\n\n/**\n * The record read back: the first page's snapshot and every event, in order. Throws when the pages\n * are not one whole record — no first page, a page count they do not match, or events out of order —\n * which is damage to a completed game, never a designed answer.\n */\nexport function readFinalizedRecord(pages: readonly GameStatHistoryPage[]): {\n readonly snapshot: FinalizedRecordSnapshot;\n readonly events: GameStatEvent[];\n} {\n const [first, ...rest] = pages;\n if (first === undefined || !isFirstRecordPage(first)) throw new Error('A finalized record starts with its first page.');\n if (first.pageCount !== pages.length) {\n throw new Error(`A finalized record of ${first.pageCount} pages was read as ${pages.length}.`);\n }\n const events = [...first.events, ...rest.flatMap((page) => page.events)];\n events.forEach((event, index) => {\n if (index > 0 && (events[index - 1] as GameStatEvent).sequence >= event.sequence) {\n throw new Error(`A finalized record holds event ${event.eventId} out of sequence.`);\n }\n });\n const snapshot: FinalizedRecordSnapshot = {\n leagueType: first.leagueType,\n teamIds: first.teamIds,\n rosterPlayerIds: first.rosterPlayerIds,\n absentRosterPlayerIds: first.absentRosterPlayerIds,\n nonScoringRosterPlayerIds: first.nonScoringRosterPlayerIds,\n };\n return { snapshot, events };\n}\n\n/**\n * The completed game a record describes: its events replayed through the sport's adapter, with the\n * snapshot finalize stored. Every path after finalize derives a game's contribution from this, never\n * from the game document.\n */\nexport function finalizedGameFromRecord(adapter: AnySportAdapter, pages: readonly GameStatHistoryPage[]): FinalizedGame {\n const { snapshot, events } = readFinalizedRecord(pages);\n return {\n leagueType: snapshot.leagueType,\n teamIds: snapshot.teamIds,\n stats: replayGameStats(adapter, snapshot, events),\n rosterPlayerIds: snapshot.rosterPlayerIds,\n absentRosterPlayerIds: snapshot.absentRosterPlayerIds,\n nonScoringRosterPlayerIds: snapshot.nonScoringRosterPlayerIds,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"football-scoring.js","sourceRoot":"","sources":["../../src/sports/football-scoring.ts"],"names":[],"mappings":"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","sourcesContent":["// The football adapter's scoring, display, and bulk-entry members. Football is team-scored: each\r\n// event adds its points to one team's score, no event names a player, and there is no finish rule —\r\n// the game is finalized by hand. Bulk entry takes each team's final score.\r\n//\r\n// `apply` and `undo` are one function over an event's points with opposite signs, so they are exact\r\n// inverses by construction (SPORT-103).\r\n\r\nimport { z } from 'zod';\r\nimport { FOOTBALL_POINTS_BY_EVENT_TYPE, MAX_STAT_EVENTS_PER_WRITE, TEAM_NOT_IN_GAME_MESSAGE } from '../constants/sports.js';\r\nimport type { FootballTeamStats, GameStatEvent } from '../doc-schemas/game-stats.js';\r\nimport type { FootballGameStats, GameStats } from '../doc-schemas/sport-game.js';\r\nimport {\r\n FOOTBALL_BULK_EDIT_EVENT_TYPE,\r\n FOOTBALL_ONE_POINT_EVENT_TYPE,\r\n FOOTBALL_SCORE_EVENT_TYPE_KEYS,\r\n FOOTBALL_SIX_POINT_EVENT_TYPE,\r\n FOOTBALL_STAT_EVENT_TYPE_KEYS,\r\n FOOTBALL_TEAM_STAT_KEYS,\r\n FOOTBALL_TWO_POINT_EVENT_TYPE,\r\n} from '../types/sports.js';\r\nimport type { FootballStatEventType, FootballTeamStatKey } from '../types/sports.js';\r\nimport {\r\n BULK_ENTRY_TOO_LARGE_MESSAGE,\r\n bulkEntryCountField,\r\n type BulkEntryContext,\r\n type StatEventDraft,\r\n type TeamScoresBulkEntry,\r\n type TeamScoresValues,\r\n} from './bulk-entry.js';\r\nimport { buildGameContribution, decideParticipation, type ContributionSpec } from './contribution.js';\r\nimport {\r\n FINALIZATION_READY,\r\n STAT_EVENT_ACCEPTED,\r\n isSportStatEventIn,\r\n isStatCorrection,\r\n refuseStatEvent,\r\n type CorrectableFields,\r\n type SportScoring,\r\n type SportStatEventBody,\r\n type StatCorrectionBody,\r\n type StatEventBody,\r\n} from './scoring.js';\r\nimport {\r\n describeStoreEvent,\r\n formatSignedValue,\r\n type Scoreboard,\r\n type StatEventLabel,\r\n type StatEventNameResolver,\r\n type StatFieldLabels,\r\n type TeamPointsScoringPanel,\r\n} from './scoring-display.js';\r\n\r\ntype FootballEvent = Extract<SportStatEventBody, { type: FootballStatEventType }>;\r\n\r\nfunction isFootballEvent(event: StatEventBody | undefined): event is FootballEvent {\r\n return isSportStatEventIn(FOOTBALL_STAT_EVENT_TYPE_KEYS, event);\r\n}\r\n\r\n// The registry chose this adapter by the game's `leagueType`, so the rollup it is handed is a\r\n// football game's (SPORT-002).\r\nfunction ownStats(state: GameStats): FootballGameStats {\r\n return state as FootballGameStats;\r\n}\r\n\r\nexport function emptyFootballTeamLine(): FootballTeamStats {\r\n return Object.fromEntries(FOOTBALL_TEAM_STAT_KEYS.map((key) => [key, 0])) as FootballTeamStats;\r\n}\r\n\r\nfunction teamScoreOf(state: FootballGameStats, teamId: string): number {\r\n return (state.teamStats[teamId] ?? emptyFootballTeamLine()).score;\r\n}\r\n\r\n/** The points an event adds to its team's score: a scoring event's own, or a bulk edit's `value`. */\r\nfunction pointsOf(event: FootballEvent): number {\r\n return event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE ? event.value : FOOTBALL_POINTS_BY_EVENT_TYPE[event.type];\r\n}\r\n\r\nfunction addToScore(stateIn: GameStats, teamId: string, amount: number): FootballGameStats {\r\n const state = ownStats(stateIn);\r\n const line = state.teamStats[teamId] ?? emptyFootballTeamLine();\r\n return { teamStats: { ...state.teamStats, [teamId]: { ...line, score: line.score + amount } }, playerStats: state.playerStats };\r\n}\r\n\r\n// A correction to the team's score; football keeps no player line to correct.\r\nconst CORRECTABLE_FIELDS: CorrectableFields = { team: FOOTBALL_TEAM_STAT_KEYS, player: [] };\r\n\r\nfunction correctionAmount(event: StatCorrectionBody): number {\r\n const fields = event.playerUserId === undefined ? CORRECTABLE_FIELDS.team : CORRECTABLE_FIELDS.player;\r\n if (!fields.includes(event.stat)) {\r\n throw new Error(`A football correction cannot change ${event.stat} on a ${event.playerUserId === undefined ? 'team' : 'player'} line.`);\r\n }\r\n return event.value;\r\n}\r\n\r\nfunction namesPlayer(event: StatEventBody): boolean {\r\n return 'playerUserId' in event && event.playerUserId !== undefined;\r\n}\r\n\r\nconst validate: SportScoring['validate'] = (stateIn, event, roster) => {\r\n if (!isFootballEvent(event)) return refuseStatEvent('eventTypeNotInSport');\r\n if (!roster.teamIds.includes(event.teamId)) return refuseStatEvent('teamNotInGame');\r\n if (namesPlayer(event)) return refuseStatEvent('playerNotAllowed');\r\n if (event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE) {\r\n if (!Number.isInteger(event.value) || event.value === 0) return refuseStatEvent('bulkValueInvalid');\r\n const score = teamScoreOf(ownStats(stateIn), event.teamId);\r\n if (event.previousValue !== score) return refuseStatEvent('bulkStale');\r\n if (score + event.value < 0) return refuseStatEvent('bulkValueInvalid');\r\n }\r\n return STAT_EVENT_ACCEPTED;\r\n};\r\n\r\n// What a completed football game adds: the standings line from the two scores, and a game played\r\n// to every present roster entry, whatever its team scored. There are no individual stats, so only\r\n// absence withholds a game played and no entry is ever non-scoring.\r\nconst FOOTBALL_CONTRIBUTION: ContributionSpec = {\r\n playerStatKeys: [],\r\n teamSeasonPlayerSums: [],\r\n zeroLineIsNonScoring: false,\r\n deriveLine: (_kind, line) => line,\r\n};\r\n\r\nexport const FOOTBALL_SCORING: SportScoring = {\r\n validate,\r\n apply: (state, event) => {\r\n if (isStatCorrection(event)) return addToScore(state, event.teamId, correctionAmount(event));\r\n if (!isFootballEvent(event)) throw new Error(`The football adapter does not apply ${event.type}.`);\r\n return addToScore(state, event.teamId, pointsOf(event));\r\n },\r\n undo: (state, target) => {\r\n if (!isFootballEvent(target)) throw new Error(`The football adapter does not undo ${target.type}.`);\r\n return addToScore(state, target.teamId, -pointsOf(target));\r\n },\r\n // A football line has no derived field.\r\n deriveLine: FOOTBALL_CONTRIBUTION.deriveLine,\r\n correctableFields: CORRECTABLE_FIELDS,\r\n // No rounds and no finish rule: the game is finalized by hand.\r\n nextRound: () => null,\r\n finalizationReadiness: () => FINALIZATION_READY,\r\n participation: (state, rosterPlayerIds, absentRosterPlayerIds) =>\r\n decideParticipation(FOOTBALL_CONTRIBUTION, state, rosterPlayerIds, absentRosterPlayerIds),\r\n contribution: (game) => buildGameContribution(FOOTBALL_CONTRIBUTION, game),\r\n};\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Display\r\n// ---------------------------------------------------------------------------------------------\r\n\r\nexport const FOOTBALL_EVENT_LABELS = {\r\n [FOOTBALL_SIX_POINT_EVENT_TYPE]: { button: '+6', play: '6 points' },\r\n [FOOTBALL_TWO_POINT_EVENT_TYPE]: { button: '+2', play: '2 points' },\r\n [FOOTBALL_ONE_POINT_EVENT_TYPE]: { button: '+1', play: '1 point' },\r\n [FOOTBALL_BULK_EDIT_EVENT_TYPE]: { button: 'Final scores', play: 'Final score entry' },\r\n} as const satisfies Record<FootballStatEventType, StatEventLabel>;\r\n\r\nexport const FOOTBALL_FIELD_LABELS = {\r\n team: { score: 'Score' },\r\n player: {},\r\n} as const satisfies StatFieldLabels<FootballTeamStatKey, never>;\r\n\r\nexport function describeFootballEvent(event: GameStatEvent, resolve: StatEventNameResolver): string {\r\n const storeEvent = describeStoreEvent(event, resolve, FOOTBALL_FIELD_LABELS);\r\n if (storeEvent !== null) return storeEvent;\r\n if (!isFootballEvent(event)) return event.type;\r\n const team = resolve.team(event.teamId);\r\n if (event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE) {\r\n return `${team}: ${formatSignedValue(event.value)} points (${FOOTBALL_EVENT_LABELS[event.type].play.toLowerCase()})`;\r\n }\r\n return `${team}: ${FOOTBALL_EVENT_LABELS[event.type].play}`;\r\n}\r\n\r\nexport const FOOTBALL_SCOREBOARD: Scoreboard = {\r\n figures: [{ id: 'points', label: 'Points', value: (teamLine) => teamLine.score }],\r\n jumpBall: false,\r\n};\r\n\r\nexport const FOOTBALL_SCORING_PANEL: TeamPointsScoringPanel = {\r\n kind: 'teamPoints',\r\n eventTypes: FOOTBALL_SCORE_EVENT_TYPE_KEYS,\r\n};\r\n\r\n// ---------------------------------------------------------------------------------------------\r\n// Bulk entry — each team's final score\r\n// ---------------------------------------------------------------------------------------------\r\n\r\nfunction scoreDrafts(values: TeamScoresValues, context: BulkEntryContext): StatEventDraft[] {\r\n const state = ownStats(context.state);\r\n const drafts: StatEventDraft[] = [];\r\n for (const teamId of context.game.teamIds) {\r\n const entered = values.scores[teamId];\r\n const previousValue = teamScoreOf(state, teamId);\r\n if (entered === undefined || entered === previousValue) continue;\r\n drafts.push({ type: FOOTBALL_BULK_EDIT_EVENT_TYPE, teamId, value: entered - previousValue, previousValue });\r\n }\r\n return drafts;\r\n}\r\n\r\nexport const FOOTBALL_BULK_ENTRY: TeamScoresBulkEntry = {\r\n kind: 'teamScores',\r\n formDefaults: (context) => ({\r\n scores: Object.fromEntries(context.game.teamIds.map((teamId) => [teamId, teamScoreOf(ownStats(context.state), teamId)])),\r\n }),\r\n formSchema: (context) => {\r\n const teams = new Set(context.game.teamIds);\r\n return z.object({ scores: z.record(z.string(), bulkEntryCountField) }).superRefine((values, issues) => {\r\n for (const teamId of Object.keys(values.scores)) {\r\n if (!teams.has(teamId)) issues.addIssue({ code: 'custom', path: ['scores', teamId], message: TEAM_NOT_IN_GAME_MESSAGE });\r\n }\r\n if (scoreDrafts(values, context).length > MAX_STAT_EVENTS_PER_WRITE) {\r\n issues.addIssue({ code: 'custom', path: ['scores'], message: BULK_ENTRY_TOO_LARGE_MESSAGE });\r\n }\r\n });\r\n },\r\n toEvents: scoreDrafts,\r\n};\r\n"]}
|
|
1
|
+
{"version":3,"file":"football-scoring.js","sourceRoot":"","sources":["../../src/sports/football-scoring.ts"],"names":[],"mappings":"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","sourcesContent":["// The football adapter's scoring, display, and bulk-entry members. Football is team-scored: each\n// event adds its points to one team's score, no event names a player, and there is no finish rule —\n// the game is finalized by hand. Bulk entry takes each team's final score.\n//\n// `apply` and `undo` are one function over an event's points with opposite signs, so they are exact\n// inverses by construction (SPORT-103).\n\nimport { z } from 'zod';\nimport { FOOTBALL_POINTS_BY_EVENT_TYPE, MAX_STAT_EVENTS_PER_WRITE, TEAM_NOT_IN_GAME_MESSAGE } from '../constants/sports.js';\nimport type { FootballTeamStats, GameStatEvent } from '../doc-schemas/game-stats.js';\nimport type { FootballGameStats, GameStats } from '../doc-schemas/sport-game.js';\nimport {\n FOOTBALL_BULK_EDIT_EVENT_TYPE,\n FOOTBALL_ONE_POINT_EVENT_TYPE,\n FOOTBALL_SCORE_EVENT_TYPE_KEYS,\n FOOTBALL_SIX_POINT_EVENT_TYPE,\n FOOTBALL_STAT_EVENT_TYPE_KEYS,\n FOOTBALL_TEAM_STAT_KEYS,\n FOOTBALL_TWO_POINT_EVENT_TYPE,\n} from '../types/sports.js';\nimport type { FootballStatEventType, FootballTeamStatKey } from '../types/sports.js';\nimport {\n BULK_ENTRY_TOO_LARGE_MESSAGE,\n bulkEntryCountField,\n type BulkEntryContext,\n type StatEventDraft,\n type TeamScoresBulkEntry,\n type TeamScoresValues,\n} from './bulk-entry.js';\nimport { buildGameContribution, decideParticipation, type ContributionSpec } from './contribution.js';\nimport {\n FINALIZATION_READY,\n STAT_EVENT_ACCEPTED,\n isSportStatEventIn,\n isStatCorrection,\n refuseStatEvent,\n type CorrectableFields,\n type SportScoring,\n type SportStatEventBody,\n type StatCorrectionBody,\n type StatEventBody,\n} from './scoring.js';\nimport {\n describeStoreEvent,\n formatSignedValue,\n type Scoreboard,\n type StatEventLabel,\n type StatEventNameResolver,\n type StatFieldLabels,\n type TeamPointsScoringPanel,\n} from './scoring-display.js';\n\ntype FootballEvent = Extract<SportStatEventBody, { type: FootballStatEventType }>;\n\nfunction isFootballEvent(event: StatEventBody | undefined): event is FootballEvent {\n return isSportStatEventIn(FOOTBALL_STAT_EVENT_TYPE_KEYS, event);\n}\n\n// The registry chose this adapter by the game's `leagueType`, so the rollup it is handed is a\n// football game's (SPORT-002).\nfunction ownStats(state: GameStats): FootballGameStats {\n return state as FootballGameStats;\n}\n\nexport function emptyFootballTeamLine(): FootballTeamStats {\n return Object.fromEntries(FOOTBALL_TEAM_STAT_KEYS.map((key) => [key, 0])) as FootballTeamStats;\n}\n\nfunction teamScoreOf(state: FootballGameStats, teamId: string): number {\n return (state.teamStats[teamId] ?? emptyFootballTeamLine()).score;\n}\n\n/** The points an event adds to its team's score: a scoring event's own, or a bulk edit's `value`. */\nfunction pointsOf(event: FootballEvent): number {\n return event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE ? event.value : FOOTBALL_POINTS_BY_EVENT_TYPE[event.type];\n}\n\nfunction addToScore(stateIn: GameStats, teamId: string, amount: number): FootballGameStats {\n const state = ownStats(stateIn);\n const line = state.teamStats[teamId] ?? emptyFootballTeamLine();\n return { teamStats: { ...state.teamStats, [teamId]: { ...line, score: line.score + amount } }, playerStats: state.playerStats };\n}\n\n// A correction to the team's score; football keeps no player line to correct.\nconst CORRECTABLE_FIELDS: CorrectableFields = { team: FOOTBALL_TEAM_STAT_KEYS, player: [] };\n\nfunction correctionAmount(event: StatCorrectionBody): number {\n const fields = event.playerUserId === undefined ? CORRECTABLE_FIELDS.team : CORRECTABLE_FIELDS.player;\n if (!fields.includes(event.stat)) {\n throw new Error(`A football correction cannot change ${event.stat} on a ${event.playerUserId === undefined ? 'team' : 'player'} line.`);\n }\n return event.value;\n}\n\nfunction namesPlayer(event: StatEventBody): boolean {\n return 'playerUserId' in event && event.playerUserId !== undefined;\n}\n\nconst validate: SportScoring['validate'] = (stateIn, event, roster) => {\n if (!isFootballEvent(event)) return refuseStatEvent('eventTypeNotInSport');\n if (!roster.teamIds.includes(event.teamId)) return refuseStatEvent('teamNotInGame');\n if (namesPlayer(event)) return refuseStatEvent('playerNotAllowed');\n if (event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE) {\n if (!Number.isInteger(event.value) || event.value === 0) return refuseStatEvent('bulkValueInvalid');\n const score = teamScoreOf(ownStats(stateIn), event.teamId);\n if (event.previousValue !== score) return refuseStatEvent('bulkStale');\n if (score + event.value < 0) return refuseStatEvent('bulkValueInvalid');\n }\n return STAT_EVENT_ACCEPTED;\n};\n\n// What a completed football game adds: the standings line from the two scores, and a game played\n// to every present roster entry, whatever its team scored. There are no individual stats, so only\n// absence withholds a game played and no entry is ever non-scoring.\nconst FOOTBALL_CONTRIBUTION: ContributionSpec = {\n playerStatKeys: [],\n teamSeasonPlayerSums: [],\n zeroLineIsNonScoring: false,\n deriveLine: (_kind, line) => line,\n};\n\nexport const FOOTBALL_SCORING: SportScoring = {\n validate,\n apply: (state, event) => {\n if (isStatCorrection(event)) return addToScore(state, event.teamId, correctionAmount(event));\n if (!isFootballEvent(event)) throw new Error(`The football adapter does not apply ${event.type}.`);\n return addToScore(state, event.teamId, pointsOf(event));\n },\n undo: (state, target) => {\n if (!isFootballEvent(target)) throw new Error(`The football adapter does not undo ${target.type}.`);\n return addToScore(state, target.teamId, -pointsOf(target));\n },\n // A football line has no derived field.\n deriveLine: FOOTBALL_CONTRIBUTION.deriveLine,\n correctableFields: CORRECTABLE_FIELDS,\n // No rounds and no finish rule: the game is finalized by hand.\n nextRound: () => null,\n finalizationReadiness: () => FINALIZATION_READY,\n participation: (state, rosterPlayerIds, absentRosterPlayerIds) =>\n decideParticipation(FOOTBALL_CONTRIBUTION, state, rosterPlayerIds, absentRosterPlayerIds),\n contribution: (game) => buildGameContribution(FOOTBALL_CONTRIBUTION, game),\n};\n\n// ---------------------------------------------------------------------------------------------\n// Display\n// ---------------------------------------------------------------------------------------------\n\nexport const FOOTBALL_EVENT_LABELS = {\n [FOOTBALL_SIX_POINT_EVENT_TYPE]: { button: '+6', play: '6 points' },\n [FOOTBALL_TWO_POINT_EVENT_TYPE]: { button: '+2', play: '2 points' },\n [FOOTBALL_ONE_POINT_EVENT_TYPE]: { button: '+1', play: '1 point' },\n [FOOTBALL_BULK_EDIT_EVENT_TYPE]: { button: 'Final scores', play: 'Final score entry' },\n} as const satisfies Record<FootballStatEventType, StatEventLabel>;\n\nexport const FOOTBALL_FIELD_LABELS = {\n team: { score: 'Score' },\n player: {},\n} as const satisfies StatFieldLabels<FootballTeamStatKey, never>;\n\nexport function describeFootballEvent(event: GameStatEvent, resolve: StatEventNameResolver): string {\n const storeEvent = describeStoreEvent(event, resolve, FOOTBALL_FIELD_LABELS);\n if (storeEvent !== null) return storeEvent;\n if (!isFootballEvent(event)) return event.type;\n const team = resolve.team(event.teamId);\n if (event.type === FOOTBALL_BULK_EDIT_EVENT_TYPE) {\n return `${team}: ${formatSignedValue(event.value)} points (${FOOTBALL_EVENT_LABELS[event.type].play.toLowerCase()})`;\n }\n return `${team}: ${FOOTBALL_EVENT_LABELS[event.type].play}`;\n}\n\nexport const FOOTBALL_SCOREBOARD: Scoreboard = {\n figures: [{ id: 'points', label: 'Points', value: (teamLine) => teamLine.score }],\n jumpBall: false,\n};\n\nexport const FOOTBALL_SCORING_PANEL: TeamPointsScoringPanel = {\n kind: 'teamPoints',\n eventTypes: FOOTBALL_SCORE_EVENT_TYPE_KEYS,\n};\n\n// ---------------------------------------------------------------------------------------------\n// Bulk entry — each team's final score\n// ---------------------------------------------------------------------------------------------\n\nfunction scoreDrafts(values: TeamScoresValues, context: BulkEntryContext): StatEventDraft[] {\n const state = ownStats(context.state);\n const drafts: StatEventDraft[] = [];\n for (const teamId of context.game.teamIds) {\n const entered = values.scores[teamId];\n const previousValue = teamScoreOf(state, teamId);\n if (entered === undefined || entered === previousValue) continue;\n drafts.push({ type: FOOTBALL_BULK_EDIT_EVENT_TYPE, teamId, value: entered - previousValue, previousValue });\n }\n return drafts;\n}\n\nexport const FOOTBALL_BULK_ENTRY: TeamScoresBulkEntry = {\n kind: 'teamScores',\n formDefaults: (context) => ({\n scores: Object.fromEntries(context.game.teamIds.map((teamId) => [teamId, teamScoreOf(ownStats(context.state), teamId)])),\n }),\n formSchema: (context) => {\n const teams = new Set(context.game.teamIds);\n return z.object({ scores: z.record(z.string(), bulkEntryCountField) }).superRefine((values, issues) => {\n for (const teamId of Object.keys(values.scores)) {\n if (!teams.has(teamId)) issues.addIssue({ code: 'custom', path: ['scores', teamId], message: TEAM_NOT_IN_GAME_MESSAGE });\n }\n if (scoreDrafts(values, context).length > MAX_STAT_EVENTS_PER_WRITE) {\n issues.addIssue({ code: 'custom', path: ['scores'], message: BULK_ENTRY_TOO_LARGE_MESSAGE });\n }\n });\n },\n toEvents: scoreDrafts,\n};\n"]}
|
package/dist/sports/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sports/index.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,6BAA6B;AAE7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["// The sport adapters. A sport is selected only through the registry (SPORT-002), so the individual\
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sports/index.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,6BAA6B;AAE7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["// The sport adapters. A sport is selected only through the registry (SPORT-002), so the individual\n// adapters are not exported.\n\nexport * from './column-catalog.js';\nexport * from './sport-adapter.js';\nexport * from './shared-columns.js';\nexport * from './scoring.js';\nexport * from './scoring-display.js';\nexport * from './bulk-entry.js';\nexport * from './replay.js';\nexport * from './store-driver.js';\nexport * from './contribution.js';\nexport * from './finalized-record.js';\nexport * from './rankings.js';\nexport * from './rollup-check.js';\nexport type { LifetimeSummary, LifetimeSummaryAverage, LifetimeSummaryTotal } from './lifetime-summary.js';\nexport { basketballFantasyPoints } from './basketball-scoring.js';\nexport { SPORT_ADAPTERS, SPORT_ADAPTER_LIST, UnknownSportError, getSportAdapter } from './registry.js';\nexport { gameScoreMirror } from './score-mirror.js';\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rankings.js","sourceRoot":"","sources":["../../src/sports/rankings.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,wDAAwD;AACxD,mGAAmG;AACnG,iGAAiG;AACjG,qCAAqC;AAErC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAYzD,kGAAkG;AAClG,MAAM,UAAU,iBAAiB,CAC/B,KAAoH;IAEpH,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,CACrD,MAAM,CAAC,OAAO,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAC5F,CAAC;AACJ,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,KAKG;IAEH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,qBAAqB,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,SAAS;QAC9E,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IAChH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAC7B,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CACrG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB,EAAE,KAA+B;IACvF,MAAM,MAAM,GAAG,oBAAoB,CACjC,OAAO,CAAC,UAAU,EAClB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,CAAC,CAAC,CACtF,CAAC;IACF,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE;QACvE,MAAM,KAAK,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;QAC/C,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// The league's player rankings: one row per team a player played for, for the whole season or for\
|
|
1
|
+
{"version":3,"file":"rankings.js","sourceRoot":"","sources":["../../src/sports/rankings.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,wDAAwD;AACxD,mGAAmG;AACnG,iGAAiG;AACjG,qCAAqC;AAErC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAYzD,kGAAkG;AAClG,MAAM,UAAU,iBAAiB,CAC/B,KAAoH;IAEpH,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,CACrD,MAAM,CAAC,OAAO,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAC5F,CAAC;AACJ,CAAC;AASD;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,KAKG;IAEH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,qBAAqB,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,SAAS;QAC9E,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IAChH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAC7B,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CACrG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB,EAAE,KAA+B;IACvF,MAAM,MAAM,GAAG,oBAAoB,CACjC,OAAO,CAAC,UAAU,EAClB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,CAAC,CAAC,CACtF,CAAC;IACF,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE;QACvE,MAAM,KAAK,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;QAC/C,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// The league's player rankings: one row per team a player played for, for the whole season or for\n// one saved week. The season rows are the teams' stored\n// per-membership rows; a week's rows are derived at read, never stored — the contributions of that\n// week's completed games summed through the same mapping finalize applies, so a week's rows over\n// every week add up to the season's.\n\nimport { COMPLETED_GAME_STATUS } from '../types/unions.js';\nimport type { GameStatus } from '../types/unions.js';\nimport { parseGameRosterPlayerId } from '../ids/compound-ids.js';\nimport { sumGameContributions } from './contribution.js';\nimport type { FinalizedGame } from './contribution.js';\nimport type { StatLineValues } from './scoring.js';\nimport type { AnySportAdapter } from './sport-adapter.js';\n\n/** One rankings row: a person's games for one team, keyed by the membership the team, league, and person name. */\nexport interface RankingRow {\n readonly teamId: string;\n readonly userId: string;\n readonly line: StatLineValues;\n}\n\n/** The season rows: every team's stored per-membership rows, one per team a player played for. */\nexport function seasonRankingRows(\n teams: readonly { readonly teamId: string; readonly playerSeasonStats?: Readonly<Record<string, StatLineValues>> }[],\n): RankingRow[] {\n return teams.flatMap(({ teamId, playerSeasonStats }) =>\n Object.entries(playerSeasonStats ?? {}).map(([userId, line]) => ({ teamId, userId, line })),\n );\n}\n\n/** A saved schedule week as the rankings' week filter offers it. */\nexport interface PlayedWeek {\n readonly weekId: string;\n readonly weekTitle: string;\n readonly weekStartDate: number;\n}\n\n/**\n * The weeks the filter offers: every saved week holding at least one completed game, in the order\n * the weeks start, taken from the game documents the league schedule already loads.\n */\nexport function playedWeeks(\n games: readonly {\n readonly weekId: string;\n readonly weekTitle: string;\n readonly weekStartDate: number;\n readonly status: GameStatus;\n }[],\n): PlayedWeek[] {\n const weeks = new Map<string, PlayedWeek>();\n for (const game of games) {\n if (game.status !== COMPLETED_GAME_STATUS || weeks.has(game.weekId)) continue;\n weeks.set(game.weekId, { weekId: game.weekId, weekTitle: game.weekTitle, weekStartDate: game.weekStartDate });\n }\n return [...weeks.values()].sort(\n (left, right) => left.weekStartDate - right.weekStartDate || left.weekId.localeCompare(right.weekId),\n );\n}\n\n/**\n * One week's rows: the summed contributions of the week's completed games, each read from its\n * finalized record (`finalizedGameFromRecord`). A player who played both sides of a game that week\n * has a row on each team.\n */\nexport function weekRankingRows(adapter: AnySportAdapter, games: readonly FinalizedGame[]): RankingRow[] {\n const summed = sumGameContributions(\n adapter.deriveLine,\n games.map((game) => ({ contribution: adapter.contribution(game), sign: 1 as const })),\n );\n return Object.entries(summed.membershipRows).flatMap(([entryId, line]) => {\n const entry = parseGameRosterPlayerId(entryId);\n return entry === null ? [] : [{ teamId: entry.teamId, userId: entry.userId, line }];\n });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollup-check.js","sourceRoot":"","sources":["../../src/sports/rollup-check.ts"],"names":[],"mappings":"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","sourcesContent":["// Whether a game's stored numbers are what produced them: the rollup's counters and the score mirror\
|
|
1
|
+
{"version":3,"file":"rollup-check.js","sourceRoot":"","sources":["../../src/sports/rollup-check.ts"],"names":[],"mappings":"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","sourcesContent":["// Whether a game's stored numbers are what produced them: the rollup's counters and the score mirror\n// against a replay — of the event documents for a live game, of the finalized record for a completed\n// one. Finalize, reopen, a ghost link, and validation ask this one comparison, so a hand edit to the\n// game document is found the same way everywhere and never adopted or discarded silently (ENG-009).\n//\n// Counters are compared, never a derived field: a stored FP a weight change left stale is not a\n// difference in what was recorded, and the writer re-derives it.\n\nimport type { GameStats } from '../doc-schemas/sport-game.js';\nimport { gameScoreMirror } from './score-mirror.js';\nimport { isAllZeroLine } from './scoring.js';\nimport type { StatLineValues } from './scoring.js';\nimport type { AnySportAdapter } from './sport-adapter.js';\n\n/** One place a stored rollup or score mirror differs from the replay. */\nexport interface RollupDifference {\n readonly kind: 'rollupMismatch' | 'scoreMirrorMismatch';\n /** The team id of a team line or score, or the roster-entry id of a player line. */\n readonly key: string;\n readonly field: string;\n readonly stored: unknown;\n readonly replayed: unknown;\n}\n\n/** Two stored line values alike: a number, or a list of round slots, slot by slot. */\nfunction sameValue(left: unknown, right: unknown): boolean {\n if (Array.isArray(left) && Array.isArray(right)) {\n return left.length === right.length && left.every((slot, index) => Object.is(slot, right[index]));\n }\n return Object.is(left, right);\n}\n\n/** The fields a line is compared on: every field of a team line, the counters of a player line. */\nfunction compared(line: StatLineValues | undefined, fields: readonly string[] | null): Record<string, unknown> {\n if (line === undefined) return {};\n const keys = fields ?? Object.keys(line);\n return Object.fromEntries(keys.filter((field) => line[field] !== undefined).map((field) => [field, line[field]]));\n}\n\n/**\n * Every difference between the stored rollup and score mirror and the replay. A player line that\n * holds nothing and one that is absent are alike; a team line is compared whole, its round slots\n * included, for each of the game's teams.\n */\nexport function rollupDifferences(\n adapter: AnySportAdapter,\n game: { readonly teamIds: readonly string[]; readonly stats: GameStats; readonly score: Readonly<Record<string, number>> },\n replayed: GameStats,\n): RollupDifference[] {\n const differences: RollupDifference[] = [];\n const storedTeams = game.stats.teamStats as Readonly<Record<string, StatLineValues>>;\n const replayedTeams = replayed.teamStats as Readonly<Record<string, StatLineValues>>;\n const teamKeys = new Set([...game.teamIds, ...Object.keys(storedTeams)]);\n for (const teamId of teamKeys) {\n const stored = compared(storedTeams[teamId], null);\n const replay = compared(replayedTeams[teamId], null);\n for (const field of new Set([...Object.keys(stored), ...Object.keys(replay)])) {\n if (!sameValue(stored[field], replay[field])) {\n differences.push({ kind: 'rollupMismatch', key: teamId, field, stored: stored[field], replayed: replay[field] });\n }\n }\n }\n\n const counters = adapter.playerStatKeys as readonly string[];\n const storedPlayers = game.stats.playerStats as Readonly<Record<string, StatLineValues>>;\n const replayedPlayers = replayed.playerStats as Readonly<Record<string, StatLineValues>>;\n const holding = (lines: Readonly<Record<string, StatLineValues>>) =>\n Object.keys(lines).filter((entryId) => !isAllZeroLine(lines[entryId] as StatLineValues, counters));\n for (const entryId of new Set([...holding(storedPlayers), ...holding(replayedPlayers)])) {\n const stored = compared(storedPlayers[entryId], counters);\n const replay = compared(replayedPlayers[entryId], counters);\n for (const field of counters) {\n const [left, right] = [stored[field] ?? 0, replay[field] ?? 0];\n if (left !== right) differences.push({ kind: 'rollupMismatch', key: entryId, field, stored: left, replayed: right });\n }\n }\n\n const mirror = gameScoreMirror(replayed as { teamStats: Record<string, { score: number }> });\n for (const teamId of new Set([...Object.keys(game.score), ...Object.keys(mirror)])) {\n if (game.score[teamId] !== mirror[teamId]) {\n differences.push({\n kind: 'scoreMirrorMismatch',\n key: teamId,\n field: 'score',\n stored: game.score[teamId],\n replayed: mirror[teamId],\n });\n }\n }\n return differences;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/sports/scoring.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,+FAA+F;AAC/F,4DAA4D;AAC5D,EAAE;AACF,qGAAqG;AACrG,kGAAkG;AAClG,oGAAoG;AACpG,2CAA2C;AAK3C,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AA2BrF,MAAM,UAAU,UAAU,CAAC,KAAoB;IAC7C,OAAO,KAAK,CAAC,IAAI,KAAK,oBAAoB,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,OAAO,KAAK,CAAC,IAAI,KAAK,0BAA0B,CAAC;AACnD,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,kBAAkB,CAChC,UAA6B,EAC7B,KAAgC;IAEhC,OAAO,KAAK,KAAK,SAAS,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAChE,CAAC;AAoBD,MAAM,CAAC,MAAM,mBAAmB,GAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AAElE,MAAM,UAAU,eAAe,CAAC,IAA0B;IACxD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC7B,CAAC;AAMD,MAAM,CAAC,MAAM,kBAAkB,GAA0B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AA6CzE,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,IAAoB,EAAE,WAA8B;IAChF,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC;AAChF,CAAC","sourcesContent":["// The scoring half of the sport adapter contract: the pure members the store driver, the replay, the\
|
|
1
|
+
{"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/sports/scoring.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,+FAA+F;AAC/F,4DAA4D;AAC5D,EAAE;AACF,qGAAqG;AACrG,kGAAkG;AAClG,oGAAoG;AACpG,2CAA2C;AAK3C,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AA2BrF,MAAM,UAAU,UAAU,CAAC,KAAoB;IAC7C,OAAO,KAAK,CAAC,IAAI,KAAK,oBAAoB,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAoB;IACnD,OAAO,KAAK,CAAC,IAAI,KAAK,0BAA0B,CAAC;AACnD,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,kBAAkB,CAChC,UAA6B,EAC7B,KAAgC;IAEhC,OAAO,KAAK,KAAK,SAAS,IAAI,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAChE,CAAC;AAoBD,MAAM,CAAC,MAAM,mBAAmB,GAAqB,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AAElE,MAAM,UAAU,eAAe,CAAC,IAA0B;IACxD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC7B,CAAC;AAMD,MAAM,CAAC,MAAM,kBAAkB,GAA0B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AA6CzE,0FAA0F;AAC1F,MAAM,UAAU,aAAa,CAAC,IAAoB,EAAE,WAA8B;IAChF,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC;AAChF,CAAC","sourcesContent":["// The scoring half of the sport adapter contract: the pure members the store driver, the replay, the\n// client's optimistic queue, and the aggregate cores call (SPORT-003). None issues a Firestore\n// operation or an increment, reads a clock, or mints an id.\n//\n// Every member is typed over every sport's shapes, so any registered adapter is called the same way.\n// The registry chose the adapter by the game's `leagueType`, which is what makes the rollup it is\n// handed that sport's (SPORT-002); an event of another sport is refused by `validate` and thrown on\n// by `apply` and `undo`, never guessed at.\n\nimport type { GameStatEvent } from '../doc-schemas/game-stats.js';\nimport type { GameStats } from '../doc-schemas/sport-game.js';\nimport type { StatCounterKey } from '../types/sports.js';\nimport { STAT_CORRECTION_EVENT_TYPE, STAT_VOID_EVENT_TYPE } from '../types/stats.js';\nimport type {\n FinalizationNotReadyReason,\n StatEventRecorderField,\n StatEventRefusalCode,\n StatEventSnapshotField,\n StatLineKind,\n} from '../types/stats.js';\nimport type { FinalizedGame, GameContribution } from './contribution.js';\nimport type { GameTeamIds } from './sport-adapter.js';\n\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\n\n/**\n * What an event says happened — its type and its payload. Its id, its `recordedAt`, and what the\n * writing core stamps on it are not part of it, so a stored event, a queued one, and a drafted one\n * are all bodies.\n */\nexport type StatEventBody = DistributiveOmit<\n GameStatEvent,\n 'eventId' | 'recordedAt' | StatEventRecorderField | StatEventSnapshotField\n>;\nexport type StatVoidBody = Extract<StatEventBody, { type: typeof STAT_VOID_EVENT_TYPE }>;\nexport type StatCorrectionBody = Extract<StatEventBody, { type: typeof STAT_CORRECTION_EVENT_TYPE }>;\n/** One of a sport's own events, as opposed to the store-level void and correction. */\nexport type SportStatEventBody = Exclude<StatEventBody, StatVoidBody | StatCorrectionBody>;\n\nexport function isStatVoid(event: StatEventBody): event is StatVoidBody {\n return event.type === STAT_VOID_EVENT_TYPE;\n}\n\nexport function isStatCorrection(event: StatEventBody): event is StatCorrectionBody {\n return event.type === STAT_CORRECTION_EVENT_TYPE;\n}\n\n/** Whether `event` is one of the sport events `eventTypes` declares — never a void or a correction. */\nexport function isSportStatEventIn(\n eventTypes: readonly string[],\n event: StatEventBody | undefined,\n): event is SportStatEventBody {\n return event !== undefined && eventTypes.includes(event.type);\n}\n\n/** One team's line in a game's rollup, whichever sport it counts. */\nexport type GameTeamLine = GameStats['teamStats'][string];\n/** One roster entry's line in a game's rollup, whichever sport it counts. */\nexport type GamePlayerLine = GameStats['playerStats'][string];\n\n/** A stat line of any kind as `deriveLine` reads and returns it: its counters, derived fields, and ends. */\nexport type StatLineValues = { readonly [field: string]: number | readonly (number | null)[] };\n\n/** The game an event is validated against: its teams, and the roster entries the batch names. */\nexport interface StatEventRoster {\n /** A team of the game is one of these — never inferred from which team lines a rollup holds. */\n readonly teamIds: readonly string[];\n /** The roster entries the driver read for the batch — never the whole roster. */\n readonly rosterPlayerIds: ReadonlySet<string>;\n}\n\nexport type StatEventVerdict = { readonly ok: true } | { readonly ok: false; readonly code: StatEventRefusalCode };\n\nexport const STAT_EVENT_ACCEPTED: StatEventVerdict = { ok: true };\n\nexport function refuseStatEvent(code: StatEventRefusalCode): StatEventVerdict {\n return { ok: false, code };\n}\n\nexport type FinalizationReadiness =\n | { readonly ready: true }\n | { readonly ready: false; readonly reason: FinalizationNotReadyReason };\n\nexport const FINALIZATION_READY: FinalizationReadiness = { ready: true };\n\n/** The fields a `STAT_CORRECTION` may change, on a team line and on a player line. */\nexport interface CorrectableFields {\n readonly team: readonly StatCounterKey[];\n readonly player: readonly StatCounterKey[];\n}\n\nexport interface SportScoring {\n /**\n * Whether a new event may be recorded against `state`: its type is the sport's, its team is one\n * of the game's, every person it names is a roster entry of that team, and its values are a real\n * result. Only the store driver calls it, and only for a new event — a replay never re-validates\n * what was already accepted.\n */\n readonly validate: (state: GameStats, event: StatEventBody, roster: StatEventRoster) => StatEventVerdict;\n /**\n * `state` with one sport event or one `STAT_CORRECTION` applied. A missing line is created from\n * the sport's empty line (SPORT-105); a player line whose counters all return to zero is dropped;\n * a team line never is. Throws on a void, another sport's event, or a correction to a field not in\n * `correctableFields`.\n */\n readonly apply: (state: GameStats, event: StatEventBody) => GameStats;\n /** The exact inverse of `apply(target)` for one of the sport's own events (SPORT-103). */\n readonly undo: (state: GameStats, target: StatEventBody) => GameStats;\n /** `line` with every derived field of its kind; the only caller of any derivation formula. */\n readonly deriveLine: (kind: StatLineKind, line: StatLineValues) => StatLineValues;\n readonly correctableFields: CorrectableFields;\n /** The next round to record for a sport played in rounds, `null` once all are or for any other sport. */\n readonly nextRound: (state: GameStats) => number | null;\n /** Whether the game may be finalized (SPORT-202). */\n readonly finalizationReadiness: (state: GameStats, game: GameTeamIds) => FinalizationReadiness;\n /**\n * The games-played decision finalize stores: the roster entries credited no game played, from\n * each entry's own line and absence, in roster order (SPORT-204).\n */\n readonly participation: (\n state: GameStats,\n rosterPlayerIds: readonly string[],\n absentRosterPlayerIds: readonly string[],\n ) => string[];\n /** What a completed game adds to every derived destination (SPORT-207). */\n readonly contribution: (game: FinalizedGame) => GameContribution;\n}\n\n/** Whether every counter of a line is zero — a player line the rollup then leaves out. */\nexport function isAllZeroLine(line: StatLineValues, counterKeys: readonly string[]): boolean {\n return counterKeys.every((key) => line[key] === 0 || line[key] === undefined);\n}\n"]}
|