courthive-components 3.15.1 → 4.0.0
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/components/inline-scoring/index.d.ts +1 -0
- package/dist/components/inline-scoring/isScorable.d.ts +37 -0
- package/dist/components/inline-scoring/renderInlineMatchUp.d.ts +4 -2
- package/dist/components/pressureChart/buildPressureSeries.d.ts +21 -0
- package/dist/components/pressureChart/drawGraph.d.ts +41 -0
- package/dist/components/pressureChart/getActualPressure.d.ts +14 -0
- package/dist/components/pressureChart/getProjectedPressure.d.ts +23 -0
- package/dist/components/pressureChart/index.d.ts +18 -0
- package/dist/components/pressureChart/pathDifficultyBar.d.ts +17 -0
- package/dist/components/pressureChart/pressureChart.d.ts +40 -0
- package/dist/components/pressureChart/pressureSmallMultiples.d.ts +17 -0
- package/dist/components/pressureChart/pressureTable.d.ts +2 -0
- package/dist/components/pressureChart/ratingScale.d.ts +44 -0
- package/dist/components/pressureChart/types.d.ts +153 -0
- package/dist/components/pressureChart/winProbability.d.ts +30 -0
- package/dist/components/tournament-card/types.d.ts +8 -0
- package/dist/courthive-components.css +1 -1
- package/dist/courthive-components.es.js +7500 -6613
- package/dist/courthive-components.umd.js +44 -44
- package/dist/index.d.ts +3 -1
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { InlineScoringManager } from './inlineScoringManager';
|
|
2
2
|
export { renderInlineMatchUp } from './renderInlineMatchUp';
|
|
3
|
+
export { isScorable, sideParticipantName } from './isScorable';
|
|
3
4
|
export { engineToMatchUp } from './engineToMatchUp';
|
|
4
5
|
export { createInlineScoringFooter } from './inlineScoringFooter';
|
|
5
6
|
export type { InlineScoringMode, InlineScoringConfig, InlineScoringCallbacks, InlineScoringEngineState } from './inlineScoringTypes';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MatchUp } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* The name a side's participant is known by, or `undefined` when there isn't one.
|
|
4
|
+
*
|
|
5
|
+
* Returns `undefined` rather than a placeholder ON PURPOSE. `"Side 1"` reads like a legitimate name
|
|
6
|
+
* everywhere it is subsequently displayed or persisted, so a fallback here is indistinguishable from
|
|
7
|
+
* a real record. A refusal you can see beats a plausible value you cannot.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sideParticipantName(matchUp: MatchUp | undefined, sideNumber: number): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* May this matchUp be offered for interactive scoring at all?
|
|
12
|
+
*
|
|
13
|
+
* FAIL CLOSED. A scoring affordance leads to stored results, and the two things that make a stored
|
|
14
|
+
* result trustworthy cannot be reconstructed afterwards:
|
|
15
|
+
*
|
|
16
|
+
* - **`matchUpFormat`** decides how a score is *interpreted*. It is determined by the tournamentRecord
|
|
17
|
+
* and delivered by the factory; there is no correct way to guess it. A score kept against an invented
|
|
18
|
+
* format is not slightly wrong, it is uninterpretable.
|
|
19
|
+
* - **Named participants.** A result attributed to "Side 1" is indistinguishable from a genuine one at
|
|
20
|
+
* every point downstream.
|
|
21
|
+
*
|
|
22
|
+
* So a matchUp missing either is not scored with a substitute — it is not offered for scoring. That is
|
|
23
|
+
* the only arrangement in which the substitute cannot exist.
|
|
24
|
+
*
|
|
25
|
+
* This lives in the LIBRARY rather than in each app deliberately. Both consumers had grown their own
|
|
26
|
+
* weaker version of this check (participant existence, no name check, no format check), and
|
|
27
|
+
* `renderInlineMatchUp` — the single place a scoring affordance is minted — defaulted the format for
|
|
28
|
+
* everyone. A guard beside the thing it guards is structural; a guard in each app is a rule each app
|
|
29
|
+
* has to remember.
|
|
30
|
+
*
|
|
31
|
+
* Costs nothing in practice: across a real production tournament (three events, 211 matchUps) every
|
|
32
|
+
* matchUp carried a `matchUpFormat`, and the set passing this gate was exactly the set with two
|
|
33
|
+
* hydrated, named sides.
|
|
34
|
+
*
|
|
35
|
+
* @param matchUpFormat optional override, matching `renderInlineMatchUp`'s parameter of the same name.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isScorable(matchUp: MatchUp | undefined, matchUpFormat?: string): boolean;
|
|
@@ -21,7 +21,9 @@ interface RenderInlineMatchUpParams {
|
|
|
21
21
|
* The footer buttons (Undo/Redo/Clear/Submit) are rendered by `renderMatchUp` itself,
|
|
22
22
|
* driven by the inlineScoring config state and eventHandlers passed down.
|
|
23
23
|
*
|
|
24
|
-
* Returns a container element that re-renders itself after each scoring action
|
|
24
|
+
* Returns a container element that re-renders itself after each scoring action, or **`null`** when the
|
|
25
|
+
* matchUp must not be offered for scoring — see `isScorable`. Callers must handle the null rather than
|
|
26
|
+
* replacing a rendered cell with it; the return type is what forces them to.
|
|
25
27
|
*/
|
|
26
|
-
export declare function renderInlineMatchUp(params: RenderInlineMatchUpParams): HTMLElement;
|
|
28
|
+
export declare function renderInlineMatchUp(params: RenderInlineMatchUpParams): HTMLElement | null;
|
|
27
29
|
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProjectedPressureResult, PressureSeries } from './types';
|
|
2
|
+
import { GetProjectedPressureParams } from './getProjectedPressure';
|
|
3
|
+
export type BuildPressureSeriesParams = GetProjectedPressureParams;
|
|
4
|
+
export type PressureSeriesResult = {
|
|
5
|
+
series: PressureSeries[];
|
|
6
|
+
projection: ProjectedPressureResult;
|
|
7
|
+
/** The rating scale the values were read from, for axis labelling. */
|
|
8
|
+
scaleName?: string;
|
|
9
|
+
unratedCount: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildPressureSeries(params: BuildPressureSeriesParams): PressureSeriesResult;
|
|
12
|
+
/**
|
|
13
|
+
* Participants ordered hardest SLOT first — the ranked "path difficulty" view,
|
|
14
|
+
* and the entry point of the all-players surface.
|
|
15
|
+
*
|
|
16
|
+
* Sorts on `slotDifficulty`, not `pathDifficulty`: the reach-weighted figure
|
|
17
|
+
* discounts the late rounds a weak player is unlikely to reach, so ranking on it
|
|
18
|
+
* would report the toughest draws in the field as the easiest. Unrated
|
|
19
|
+
* participants sort last rather than being dropped, so the field size stays honest.
|
|
20
|
+
*/
|
|
21
|
+
export declare function byPathDifficulty(series: PressureSeries[]): PressureSeries[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PressureUnsupportedReason } from './types';
|
|
2
|
+
export declare const BYE_STATUS = "BYE";
|
|
3
|
+
export type GraphSide = {
|
|
4
|
+
participantId?: string;
|
|
5
|
+
participant?: any;
|
|
6
|
+
drawPosition?: number;
|
|
7
|
+
bye: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type GraphMatchUp = {
|
|
10
|
+
matchUpId?: string;
|
|
11
|
+
roundNumber: number;
|
|
12
|
+
roundPosition: number;
|
|
13
|
+
sides: [GraphSide, GraphSide];
|
|
14
|
+
winningSide?: number;
|
|
15
|
+
matchUpStatus?: string;
|
|
16
|
+
raw: any;
|
|
17
|
+
};
|
|
18
|
+
export type EliminationGraph = {
|
|
19
|
+
roundNumbers: number[];
|
|
20
|
+
/** roundNumber -> roundPosition -> matchUp */
|
|
21
|
+
byRound: Map<number, Map<number, GraphMatchUp>>;
|
|
22
|
+
firstRound: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Build the graph, or say why it could not be built.
|
|
26
|
+
*
|
|
27
|
+
* Rejects anything that is not a clean halving tree — round-robin containers,
|
|
28
|
+
* ad-hoc structures, and fed/consolation shapes whose round sizes do not halve.
|
|
29
|
+
* Refusing is deliberate: a wrong tree would produce a plausible-looking
|
|
30
|
+
* projection with silently wrong opponents, which is worse than no chart.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildEliminationGraph(matchUps: any[]): {
|
|
33
|
+
graph?: EliminationGraph;
|
|
34
|
+
unsupported?: PressureUnsupportedReason;
|
|
35
|
+
};
|
|
36
|
+
/** The two feeder positions of `(roundNumber, roundPosition)` in the previous round. */
|
|
37
|
+
export declare function feederPositions(roundPosition: number): [number, number];
|
|
38
|
+
/** The position a winner of `roundPosition` advances into, in the next round. */
|
|
39
|
+
export declare function advancingPosition(roundPosition: number): number;
|
|
40
|
+
/** True when the matchUp is a bye for whichever side is present. */
|
|
41
|
+
export declare function isByeMatchUp(matchUp: GraphMatchUp): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ParticipantActualPressure } from './types';
|
|
2
|
+
export type GetActualPressureParams = {
|
|
3
|
+
matchUps: any[];
|
|
4
|
+
matchUpType?: string;
|
|
5
|
+
scaleName?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Walk every matchUp a participant appears in and describe what they met.
|
|
9
|
+
*
|
|
10
|
+
* Only matchUps whose sides are hydrated contribute — an unplayed later round
|
|
11
|
+
* has empty sides, so it simply produces no entry. That is the correct
|
|
12
|
+
* behaviour: this series exists to show what *happened*.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getActualPressure(params: GetActualPressureParams): ParticipantActualPressure[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { WinProbabilityModel } from './winProbability';
|
|
2
|
+
import { ProjectedPressureResult } from './types';
|
|
3
|
+
/** Opponents below this arrival probability are excluded from the displayed range. */
|
|
4
|
+
export declare const DEFAULT_RANGE_THRESHOLD = 0.01;
|
|
5
|
+
export type GetProjectedPressureParams = {
|
|
6
|
+
/** Structure matchUps, hydrated with sides + participants. */
|
|
7
|
+
matchUps: any[];
|
|
8
|
+
/** SINGLES (default) or DOUBLES — selects which ratings block to read. */
|
|
9
|
+
matchUpType?: string;
|
|
10
|
+
/** Prefer this scale when a participant carries several. */
|
|
11
|
+
scaleName?: string;
|
|
12
|
+
/** Pairwise model knobs; see `winProbability`. */
|
|
13
|
+
model?: WinProbabilityModel;
|
|
14
|
+
/**
|
|
15
|
+
* When true, a played matchUp collapses to its actual winner, so the
|
|
16
|
+
* projection reads "from here". Default **false** — the untouched projection
|
|
17
|
+
* is what the draw said at the outset, which is the comparison the chart makes.
|
|
18
|
+
*/
|
|
19
|
+
respectResults?: boolean;
|
|
20
|
+
/** Arrival-probability floor for inclusion in `opponentEloRange`. */
|
|
21
|
+
rangeThreshold?: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function getProjectedPressure(params: GetProjectedPressureParams): ProjectedPressureResult;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { buildPressureSmallMultiples, sharedYDomain } from './pressureSmallMultiples';
|
|
2
|
+
export { buildPressureSeries, byPathDifficulty } from './buildPressureSeries';
|
|
3
|
+
export { buildPressureChart, resolveYDomain, defaultRoundLabel } from './pressureChart';
|
|
4
|
+
export { getProjectedPressure, DEFAULT_RANGE_THRESHOLD } from './getProjectedPressure';
|
|
5
|
+
export { eloWinProbability, winProbability, DEFAULT_ELO_DIVISOR } from './winProbability';
|
|
6
|
+
export { resolveParticipantRating, ratingToElo, convertRange } from './ratingScale';
|
|
7
|
+
export { buildPathDifficultyBar } from './pathDifficultyBar';
|
|
8
|
+
export { getActualPressure } from './getActualPressure';
|
|
9
|
+
export { buildPressureTable } from './pressureTable';
|
|
10
|
+
export { PRESSURE_UNSUPPORTED } from './types';
|
|
11
|
+
export type { PressureSmallMultiplesOptions, PressureSmallMultiplesInstance } from './pressureSmallMultiples';
|
|
12
|
+
export type { PathDifficultyBarOptions, PathDifficultyBarInstance } from './pathDifficultyBar';
|
|
13
|
+
export type { BuildPressureSeriesParams, PressureSeriesResult } from './buildPressureSeries';
|
|
14
|
+
export type { PressureChartOptions, PressureChartInstance } from './pressureChart';
|
|
15
|
+
export type { GetProjectedPressureParams } from './getProjectedPressure';
|
|
16
|
+
export type { GetActualPressureParams } from './getActualPressure';
|
|
17
|
+
export type { WinProbabilityModel } from './winProbability';
|
|
18
|
+
export type { ParticipantPressureProjection, ParticipantActualPressure, ProjectedPressureResult, PressureUnsupportedReason, ProjectedRoundPressure, ActualRoundPressure, PressureSeriesPoint, PossibleOpponent, ResolvedRating, PressureSeries } from './types';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PressureSeries } from './types';
|
|
2
|
+
export type PathDifficultyBarOptions = {
|
|
3
|
+
width?: number;
|
|
4
|
+
rowHeight?: number;
|
|
5
|
+
labelWidth?: number;
|
|
6
|
+
/** Cap the rows rendered. When set, the count dropped is stated in the caption. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
/** Overlay the difficulty actually faced so far, where a participant has played. */
|
|
9
|
+
showActual?: boolean;
|
|
10
|
+
onSelect?: (series: PressureSeries) => void;
|
|
11
|
+
emptyMessage?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PathDifficultyBarInstance = {
|
|
14
|
+
element: HTMLElement;
|
|
15
|
+
update: (series: PressureSeries[], options?: PathDifficultyBarOptions) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildPathDifficultyBar(container: HTMLElement, series?: PressureSeries[], options?: PathDifficultyBarOptions): PathDifficultyBarInstance;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PressureSeries, PressureSeriesPoint } from './types';
|
|
2
|
+
export type PressureChartOptions = {
|
|
3
|
+
width?: number;
|
|
4
|
+
height?: number;
|
|
5
|
+
margin?: {
|
|
6
|
+
top: number;
|
|
7
|
+
right: number;
|
|
8
|
+
bottom: number;
|
|
9
|
+
left: number;
|
|
10
|
+
};
|
|
11
|
+
/** Compact mode for the small-multiples grid: no labels, no legend, thin marks. */
|
|
12
|
+
spark?: boolean;
|
|
13
|
+
/** Fix the y domain across a set of charts so small multiples are comparable. */
|
|
14
|
+
yDomain?: [number, number];
|
|
15
|
+
/** Label for the rating scale, e.g. 'WTN'. Shown on the axis. */
|
|
16
|
+
scaleName?: string;
|
|
17
|
+
showLegend?: boolean;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
roundLabels?: (roundNumber: number, index: number, total: number) => string;
|
|
20
|
+
emptyMessage?: string;
|
|
21
|
+
};
|
|
22
|
+
export type PressureChartInstance = {
|
|
23
|
+
element: HTMLElement;
|
|
24
|
+
update: (series: PressureSeries | undefined, options?: PressureChartOptions) => void;
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
};
|
|
27
|
+
/** QF / SF / F for the last three rounds, R1.. before that — matches the round-nav convention. */
|
|
28
|
+
export declare function defaultRoundLabel(_roundNumber: number, index: number, total: number): string;
|
|
29
|
+
/** Symmetric domain around zero so "playing up" and "playing down" read equally. */
|
|
30
|
+
export declare function resolveYDomain(points: PressureSeriesPoint[], override?: [number, number]): [number, number];
|
|
31
|
+
/** Round a half-extent down to a readable 1 / 2 / 5 x 10^n step. */
|
|
32
|
+
export declare function niceStep(extent: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Build (or rebuild) the chart into a container.
|
|
35
|
+
*
|
|
36
|
+
* Returns a handle so a consumer can re-render on a live update without
|
|
37
|
+
* re-querying the DOM. `update(undefined)` renders the empty state — an unrated
|
|
38
|
+
* field is a legitimate outcome, not an error.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildPressureChart(container: HTMLElement, series?: PressureSeries, options?: PressureChartOptions): PressureChartInstance;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PressureSeries } from './types';
|
|
2
|
+
export type PressureSmallMultiplesOptions = {
|
|
3
|
+
cellWidth?: number;
|
|
4
|
+
cellHeight?: number;
|
|
5
|
+
/** Cap the cells rendered; the number dropped is stated in the caption. */
|
|
6
|
+
limit?: number;
|
|
7
|
+
scaleName?: string;
|
|
8
|
+
onSelect?: (series: PressureSeries) => void;
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
};
|
|
11
|
+
export type PressureSmallMultiplesInstance = {
|
|
12
|
+
element: HTMLElement;
|
|
13
|
+
update: (series: PressureSeries[], options?: PressureSmallMultiplesOptions) => void;
|
|
14
|
+
};
|
|
15
|
+
/** One y domain across every cell, so the grid can be read as a comparison. */
|
|
16
|
+
export declare function sharedYDomain(series: PressureSeries[]): [number, number];
|
|
17
|
+
export declare function buildPressureSmallMultiples(container: HTMLElement, series?: PressureSeries[], options?: PressureSmallMultiplesOptions): PressureSmallMultiplesInstance;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ResolvedRating } from './types';
|
|
2
|
+
/** Linear map of a value from one range onto another. Mirrors factory `convertRange`. */
|
|
3
|
+
export declare function convertRange({ value, sourceRange, targetRange }: {
|
|
4
|
+
value: number;
|
|
5
|
+
sourceRange?: number[];
|
|
6
|
+
targetRange?: number[];
|
|
7
|
+
}): number;
|
|
8
|
+
/**
|
|
9
|
+
* Convert a value on `scaleName` to its ELO equivalent.
|
|
10
|
+
*
|
|
11
|
+
* Scales where a LOWER number is better (WTN, declared `range: [40, 1]`) are
|
|
12
|
+
* declared high-to-low; the factory detects that as `range[0] > range[1]` and
|
|
13
|
+
* reflects the value before mapping, which is reproduced here.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ratingToElo({ scaleName, value }: {
|
|
16
|
+
scaleName?: string;
|
|
17
|
+
value?: number;
|
|
18
|
+
}): number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a participant's rating onto the common scale.
|
|
21
|
+
*
|
|
22
|
+
* Expects the `participant.ratings` shape produced with
|
|
23
|
+
* `participantsProfile: { withScaleValues: true }` — the hydration CFS already
|
|
24
|
+
* requests in `getEventData`:
|
|
25
|
+
*
|
|
26
|
+
* ```
|
|
27
|
+
* ratings: { SINGLES: [{ scaleName: 'WTN', scaleValue: { wtnRating: 4.13 } }] }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* **Without `withScaleValues` the `ratings` key is present but empty** and the
|
|
31
|
+
* value lives in `participant.timeItems` instead; this returns `null` in that
|
|
32
|
+
* case rather than inventing a default. A blank chart is the correct outcome for
|
|
33
|
+
* an unrated field — see `PRESSURE_UNSUPPORTED.NO_RATINGS`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveParticipantRating({ participant, matchUpType, preferredScaleName }: {
|
|
36
|
+
participant?: any;
|
|
37
|
+
matchUpType?: string;
|
|
38
|
+
preferredScaleName?: string;
|
|
39
|
+
}): ResolvedRating | null;
|
|
40
|
+
/**
|
|
41
|
+
* The scale name that appears most often across a set of resolved ratings.
|
|
42
|
+
* Reported so a caller can label the axis honestly when a field is mixed-scale.
|
|
43
|
+
*/
|
|
44
|
+
export declare function dominantScaleName(ratings: (ResolvedRating | null)[]): string | undefined;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { CompetitivenessBucket } from '../competitivenessBar/types';
|
|
2
|
+
/** Why a projection could not be produced. Never guess — say which. */
|
|
3
|
+
export declare const PRESSURE_UNSUPPORTED: {
|
|
4
|
+
readonly NO_MATCHUPS: "NO_MATCHUPS";
|
|
5
|
+
readonly NOT_ELIMINATION: "NOT_ELIMINATION";
|
|
6
|
+
readonly NO_RATINGS: "NO_RATINGS";
|
|
7
|
+
};
|
|
8
|
+
export type PressureUnsupportedReason = (typeof PRESSURE_UNSUPPORTED)[keyof typeof PRESSURE_UNSUPPORTED];
|
|
9
|
+
/** A rating resolved onto the common (ELO-equivalent) scale. */
|
|
10
|
+
export type ResolvedRating = {
|
|
11
|
+
/** ELO-equivalent value, produced by the same range conversion the factory uses. */
|
|
12
|
+
elo: number;
|
|
13
|
+
/** The scale the value was read from, e.g. 'WTN' / 'UTR' / 'ELO'. */
|
|
14
|
+
scaleName: string;
|
|
15
|
+
/** The raw value as published on that scale, for display. */
|
|
16
|
+
sourceValue: number;
|
|
17
|
+
};
|
|
18
|
+
/** A participant who could arrive in the opponent slot, with the probability they do. */
|
|
19
|
+
export type PossibleOpponent = {
|
|
20
|
+
participantId: string;
|
|
21
|
+
participantName?: string;
|
|
22
|
+
probability: number;
|
|
23
|
+
elo: number | null;
|
|
24
|
+
};
|
|
25
|
+
/** One round of a participant's projected path. */
|
|
26
|
+
export type ProjectedRoundPressure = {
|
|
27
|
+
roundNumber: number;
|
|
28
|
+
/** Probability the participant plays this round at all. Round 1 is 1. */
|
|
29
|
+
reachProbability: number;
|
|
30
|
+
/**
|
|
31
|
+
* Probability-weighted mean opponent rating (ELO-equivalent) over the
|
|
32
|
+
* participants who could arrive from the sibling sub-bracket. `null` when no
|
|
33
|
+
* opponent in that sub-bracket carries a rating.
|
|
34
|
+
*/
|
|
35
|
+
expectedOpponentElo: number | null;
|
|
36
|
+
/** [min, max] over opponents whose arrival probability clears the threshold. */
|
|
37
|
+
opponentEloRange: [number, number] | null;
|
|
38
|
+
/** expectedOpponentElo - own rating. Positive = projected to play up. */
|
|
39
|
+
expectedSignedDelta: number | null;
|
|
40
|
+
/** How many distinct participants could arrive from the sibling sub-bracket. */
|
|
41
|
+
possibleOpponentCount: number;
|
|
42
|
+
/**
|
|
43
|
+
* Who could arrive, most-likely first. Drives the hover layer ("possible
|
|
44
|
+
* opponents"), and lets a test assert opponent *identity* rather than only a
|
|
45
|
+
* range — a range can be right by coincidence when sub-brackets are similar.
|
|
46
|
+
*/
|
|
47
|
+
possibleOpponents: PossibleOpponent[];
|
|
48
|
+
/** True once exactly one opponent is possible — i.e. the uncertainty has burned off. */
|
|
49
|
+
resolved: boolean;
|
|
50
|
+
/** True when this round is a BYE for the participant: no opponent, no pressure. */
|
|
51
|
+
bye: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** A participant's full projection across the structure. */
|
|
54
|
+
export type ParticipantPressureProjection = {
|
|
55
|
+
participantId: string;
|
|
56
|
+
participantName?: string;
|
|
57
|
+
drawPosition?: number;
|
|
58
|
+
/** The participant's own rating on the common scale, or `null` if unrated. */
|
|
59
|
+
rating: ResolvedRating | null;
|
|
60
|
+
rounds: ProjectedRoundPressure[];
|
|
61
|
+
/**
|
|
62
|
+
* **Experienced** difficulty: the reach-weighted mean of `expectedSignedDelta`.
|
|
63
|
+
* Answers "how hard are the matches this participant should expect to actually
|
|
64
|
+
* play?" — which is legitimately *lower* for a weak player, because the hard
|
|
65
|
+
* late rounds are discounted by their small chance of getting there.
|
|
66
|
+
*
|
|
67
|
+
* Do NOT rank a field on this. It systematically flatters weak players and so
|
|
68
|
+
* hides exactly the draw imbalance the ranked view exists to expose. Use
|
|
69
|
+
* `slotDifficulty` for that.
|
|
70
|
+
*/
|
|
71
|
+
pathDifficulty: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* **Slot** difficulty: the unweighted mean of `expectedSignedDelta` across all
|
|
74
|
+
* rounds carrying a rated opponent — i.e. how hard this bracket slot is if you
|
|
75
|
+
* had to walk the whole thing, independent of the occupant's own survival odds.
|
|
76
|
+
*
|
|
77
|
+
* This is the number the ranked "path difficulty" view sorts on.
|
|
78
|
+
*
|
|
79
|
+
* **Known limitation, measured on production data.** Because this is
|
|
80
|
+
* `mean(opponentElo) - ownElo` and the opponent mean varies far less across a
|
|
81
|
+
* field than the players' own ratings do, it is dominated by the participant's
|
|
82
|
+
* own rating: on the 2019 ITA DI Texas Men's Regional (99 rated entrants)
|
|
83
|
+
* Spearman rank correlation against own rating was **-0.986**. So ranking on it
|
|
84
|
+
* is close to ranking by rating inverted, and it does NOT by itself expose draw
|
|
85
|
+
* imbalance. Real draw-position signal is present but small — two entrants at
|
|
86
|
+
* UTR 10.42 and 10.55 in that draw differ by ~110 ELO of slot difficulty — and
|
|
87
|
+
* surfacing it needs the opponent-strength component on its own, or a residual
|
|
88
|
+
* against what a player of that rating typically faces. Not yet built.
|
|
89
|
+
*/
|
|
90
|
+
slotDifficulty: number | null;
|
|
91
|
+
/** Sum of `reachProbability` across rounds — the expected number of matches played. */
|
|
92
|
+
expectedMatchesPlayed: number;
|
|
93
|
+
};
|
|
94
|
+
export type ProjectedPressureResult = {
|
|
95
|
+
projections: ParticipantPressureProjection[];
|
|
96
|
+
/** Present only when nothing could be produced. */
|
|
97
|
+
unsupported?: PressureUnsupportedReason;
|
|
98
|
+
/** The scale ratings were read from, when a single scale dominated. */
|
|
99
|
+
scaleName?: string;
|
|
100
|
+
/** Count of entrants with no usable rating — surfaced, never silently defaulted. */
|
|
101
|
+
unratedCount: number;
|
|
102
|
+
};
|
|
103
|
+
/** One round of a participant's realised path. */
|
|
104
|
+
export type ActualRoundPressure = {
|
|
105
|
+
roundNumber: number;
|
|
106
|
+
matchUpId?: string;
|
|
107
|
+
opponentParticipantId?: string;
|
|
108
|
+
opponentParticipantName?: string;
|
|
109
|
+
opponentElo: number | null;
|
|
110
|
+
/** opponentElo - own rating. Positive = played up. */
|
|
111
|
+
signedDelta: number | null;
|
|
112
|
+
/** How close the scoreline was. Undefined until the matchUp has a winner. */
|
|
113
|
+
competitiveness?: CompetitivenessBucket;
|
|
114
|
+
won?: boolean;
|
|
115
|
+
bye: boolean;
|
|
116
|
+
};
|
|
117
|
+
export type ParticipantActualPressure = {
|
|
118
|
+
participantId: string;
|
|
119
|
+
rating: ResolvedRating | null;
|
|
120
|
+
rounds: ActualRoundPressure[];
|
|
121
|
+
/** Mean `signedDelta` across played rounds carrying a rated opponent. */
|
|
122
|
+
facedDifficulty: number | null;
|
|
123
|
+
matchesPlayed: number;
|
|
124
|
+
};
|
|
125
|
+
/** Chart-ready merge of the projected and actual series for one participant. */
|
|
126
|
+
export type PressureSeries = {
|
|
127
|
+
participantId: string;
|
|
128
|
+
participantName?: string;
|
|
129
|
+
drawPosition?: number;
|
|
130
|
+
rating: ResolvedRating | null;
|
|
131
|
+
/** Reach-weighted expected delta. Do not rank on this — see the projection type. */
|
|
132
|
+
pathDifficulty: number | null;
|
|
133
|
+
/** Unweighted slot difficulty. This is what the ranked view sorts on. */
|
|
134
|
+
slotDifficulty: number | null;
|
|
135
|
+
facedDifficulty: number | null;
|
|
136
|
+
points: PressureSeriesPoint[];
|
|
137
|
+
};
|
|
138
|
+
export type PressureSeriesPoint = {
|
|
139
|
+
roundNumber: number;
|
|
140
|
+
/** Projected band, in signed-delta space. `null` where the round carries no rated opponent. */
|
|
141
|
+
projected: {
|
|
142
|
+
expected: number | null;
|
|
143
|
+
low: number | null;
|
|
144
|
+
high: number | null;
|
|
145
|
+
};
|
|
146
|
+
reachProbability: number;
|
|
147
|
+
/** Realised signed delta, present only once the round has been played. */
|
|
148
|
+
actual: number | null;
|
|
149
|
+
competitiveness?: CompetitivenessBucket;
|
|
150
|
+
won?: boolean;
|
|
151
|
+
bye: boolean;
|
|
152
|
+
resolved: boolean;
|
|
153
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairwise win probability on the common (ELO-equivalent) scale.
|
|
3
|
+
*
|
|
4
|
+
* The standard ELO logistic:
|
|
5
|
+
*
|
|
6
|
+
* P(A beats B) = 1 / (1 + 10 ^ ((eloB - eloA) / divisor))
|
|
7
|
+
*
|
|
8
|
+
* `divisor` is a policy knob rather than a constant so it can be calibrated per
|
|
9
|
+
* federation / discipline. 400 is the ELO convention and the default here.
|
|
10
|
+
*
|
|
11
|
+
* This is an ASSUMPTION until measured. The factory ships
|
|
12
|
+
* `getPredictiveAccuracy` for exactly this purpose — it scores how well a rating
|
|
13
|
+
* scale predicts real outcomes over a corpus. Anything presented to a user as a
|
|
14
|
+
* likelihood should be calibrated against that first; this module only ever
|
|
15
|
+
* feeds *relative* opponent-strength expectations, which are far less sensitive
|
|
16
|
+
* to the divisor than a displayed percentage would be.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_ELO_DIVISOR = 400;
|
|
19
|
+
export type WinProbabilityModel = {
|
|
20
|
+
divisor?: number;
|
|
21
|
+
};
|
|
22
|
+
/** Probability that the `elo`-rated side beats the `opponentElo`-rated side. */
|
|
23
|
+
export declare function eloWinProbability(elo: number, opponentElo: number, model?: WinProbabilityModel): number;
|
|
24
|
+
/**
|
|
25
|
+
* Win probability where either side may be unrated. An unrated participant is
|
|
26
|
+
* treated as a coin flip against anyone — a deliberate, visible choice: it keeps
|
|
27
|
+
* a single unrated entrant from erasing the whole sub-bracket's projection,
|
|
28
|
+
* while `unratedCount` on the result reports how much of the field it applied to.
|
|
29
|
+
*/
|
|
30
|
+
export declare function winProbability(elo: number | null, opponentElo: number | null, model?: WinProbabilityModel): number;
|
|
@@ -24,6 +24,14 @@ export interface TierClassification {
|
|
|
24
24
|
export interface TournamentEntryFee {
|
|
25
25
|
amount: number;
|
|
26
26
|
currencyCode?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether `amount` is in the currency's smallest unit or in whole units.
|
|
29
|
+
*
|
|
30
|
+
* Optional here because a stored record written before the field existed still arrives without
|
|
31
|
+
* one — but a fee with no unit is NOT rendered as a number. See `feeFormatter`.
|
|
32
|
+
*/
|
|
33
|
+
unit?: 'MINOR' | 'MAJOR';
|
|
34
|
+
eventId?: string;
|
|
27
35
|
category?: string;
|
|
28
36
|
eventType?: string;
|
|
29
37
|
}
|