courthive-components 6.0.1 → 6.2.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/matchUpFormat/matchUpFormatLogic.d.ts +1 -1
- package/dist/components/schedule-page/domain/utils.d.ts +21 -1
- package/dist/components/schedule-page/index.d.ts +1 -1
- package/dist/components/schedule-page/types.d.ts +26 -0
- package/dist/components/scoring/approaches/dialPadLogic.d.ts +5 -0
- package/dist/components/scoring/keyboard.d.ts +24 -0
- package/dist/components/scoring/logic/dynamicSetsLogic.d.ts +26 -8
- package/dist/components/scoring/logic/irregularEnding.d.ts +156 -16
- package/dist/components/scoring/logic/outcomeProjection.d.ts +25 -0
- package/dist/components/scoring/logic/scoreEntryModel.d.ts +175 -0
- package/dist/components/scoring/logic/scoreEntrySelectors.d.ts +88 -0
- package/dist/components/scoring/logic/scoreEntryState.d.ts +163 -0
- package/dist/components/scoring/logic/scoreEntryStore.d.ts +8 -0
- package/dist/components/scoring/logic/scoreEntrySummary.d.ts +28 -0
- package/dist/components/scoring/logic/statusCodes.d.ts +34 -0
- package/dist/components/scoring/logic/tiebreakEntry.d.ts +14 -0
- package/dist/components/scoring/regions/dialPadRegion.d.ts +17 -0
- package/dist/components/scoring/regions/dynamicSetsRegion.d.ts +28 -0
- package/dist/components/scoring/regions/freeScoreRegion.d.ts +24 -0
- package/dist/components/scoring/regions/scoreLine.d.ts +2 -0
- package/dist/components/scoring/regions/scoreReadout.d.ts +11 -0
- package/dist/components/scoring/regions/setColumns.d.ts +17 -0
- package/dist/components/scoring/scoreEntryCard.d.ts +191 -0
- package/dist/components/scoring/scoreEntryDialog.d.ts +94 -0
- package/dist/components/scoring/types.d.ts +4 -0
- package/dist/components/scoring/utils/aggregateDecider.d.ts +18 -0
- package/dist/courthive-components.css +1 -1
- package/dist/courthive-components.es.js +8996 -7025
- package/dist/courthive-components.umd.js +44 -44
- package/dist/index.d.ts +4 -1
- package/dist/types.d.ts +10 -0
- package/package.json +7 -7
|
@@ -11,7 +11,7 @@ export declare const MATCH_ROOTS: readonly ["SET", "HAL", "QTR", "PER", "INN", "
|
|
|
11
11
|
export declare const MATCH_ROOT_LABELS: Record<string, string>;
|
|
12
12
|
/**
|
|
13
13
|
* Returns valid bestOf options for a given match root.
|
|
14
|
-
* SET root: [1,3,5] (
|
|
14
|
+
* SET root: [1,3,5,7,9] — odd counts; the factory parses any best-of count (#5136)
|
|
15
15
|
* Other roots: [1..12] (wide range for cross-sport)
|
|
16
16
|
*/
|
|
17
17
|
export declare function getBestOfOptionsForRoot(matchRoot?: string): number[];
|
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
import { CatalogMatchUpItem, MatchUpSide } from '../types';
|
|
1
|
+
import { CatalogMatchUpItem, MatchUpSide, ScheduleCellSide } from '../types';
|
|
2
2
|
export declare function deepClone<T>(obj: T): T;
|
|
3
3
|
export declare function matchUpLabel(item: CatalogMatchUpItem): string;
|
|
4
4
|
export declare function participantLabel(side?: MatchUpSide): string;
|
|
5
|
+
/**
|
|
6
|
+
* Everything a catalog item can be found by, as one lower-cased line.
|
|
7
|
+
*
|
|
8
|
+
* `sides[].participantName` alone left doubles players unfindable by their own
|
|
9
|
+
* name: the factory composes a PAIR's `participantName` from family names only
|
|
10
|
+
* (`Phoebus/Smith`), so a search for "Aiden Phoebus" matched his singles matchUps
|
|
11
|
+
* and silently skipped every doubles matchUp he was in. `individualParticipants`
|
|
12
|
+
* carries the given+family names that the pair name drops, so both are pushed.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* "Aiden Phoebus / Ravi Smith" — a doubles side's members, spelled out in full.
|
|
16
|
+
*
|
|
17
|
+
* Empty for a singles side and for an un-hydrated pair, which is the caller's cue to
|
|
18
|
+
* set no tooltip at all rather than an empty one.
|
|
19
|
+
*
|
|
20
|
+
* Pure and here rather than inline in the renderer so it can be tested without a DOM:
|
|
21
|
+
* the ecosystem's DOM layer is Playwright/Storybook, and `test-storybook` cannot
|
|
22
|
+
* currently run in this repo (612/612 suites fail in the runner, measured 2026-09-27).
|
|
23
|
+
*/
|
|
24
|
+
export declare function memberNamesLabel(side?: MatchUpSide | ScheduleCellSide): string;
|
|
5
25
|
export declare function matchUpSearchKey(item: CatalogMatchUpItem): string;
|
|
@@ -9,7 +9,7 @@ export { SchedulePageControl } from './controller/schedulePageControl';
|
|
|
9
9
|
export { SchedulePageStore } from './engine/schedulePageStore';
|
|
10
10
|
export { filterMatchUpCatalog, groupMatchUpCatalog, computeBaseRoundByEvent, isCompletedStatus } from './domain/matchUpCatalogProjections';
|
|
11
11
|
export { buildScheduleIssueIndex } from './domain/scheduleIssues';
|
|
12
|
-
export { matchUpLabel, participantLabel, matchUpSearchKey, deepClone } from './domain/utils';
|
|
12
|
+
export { matchUpLabel, participantLabel, matchUpSearchKey, memberNamesLabel, deepClone } from './domain/utils';
|
|
13
13
|
export { computeActiveStrip, computeActiveStripCell, computeActiveStripDropTarget, computeReschedulePlacements } from './domain/activeStrip';
|
|
14
14
|
export type { ActiveStripCell, ActiveStripCellState, ActiveStripCourtColumn, ActiveStripDropCandidate, ActiveStripDropTarget, ActiveStripGrid, ActiveStripGridMatchUp, ActiveStripStatusOptions, RescheduleCandidate, ReschedulePlacement } from './domain/activeStrip';
|
|
15
15
|
export { buildScheduleDateStrip } from './ui/dateStrip';
|
|
@@ -9,6 +9,19 @@ export interface MatchUpSide {
|
|
|
9
9
|
participantId?: string;
|
|
10
10
|
seedNumber?: number;
|
|
11
11
|
ranking?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Hydrated members of a pair or team, present on `inContext` matchUps.
|
|
14
|
+
*
|
|
15
|
+
* Load-bearing for search, not for layout. A PAIR participant's own
|
|
16
|
+
* `participantName` is composed by the factory from FAMILY NAMES ALONE
|
|
17
|
+
* (`Phoebus/Smith`), so a doubles side carries no given name anywhere — and a
|
|
18
|
+
* consumer searching for "Aiden Phoebus" could not match the one cell he is
|
|
19
|
+
* actually in. These are the names that make him findable.
|
|
20
|
+
*/
|
|
21
|
+
individualParticipants?: {
|
|
22
|
+
participantId?: string;
|
|
23
|
+
participantName?: string;
|
|
24
|
+
}[];
|
|
12
25
|
}
|
|
13
26
|
export interface CatalogMatchUpItem {
|
|
14
27
|
matchUpId: string;
|
|
@@ -238,6 +251,19 @@ export interface ScheduleCellSide {
|
|
|
238
251
|
teamName?: string;
|
|
239
252
|
/** True if this side is a BYE (no participant) */
|
|
240
253
|
bye?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Hydrated members of a pair or team, present on `inContext` matchUps.
|
|
256
|
+
*
|
|
257
|
+
* Load-bearing for search, not for layout. A PAIR participant's own
|
|
258
|
+
* `participantName` is composed by the factory from FAMILY NAMES ALONE
|
|
259
|
+
* (`Phoebus/Smith`), so a doubles side carries no given name anywhere — and a
|
|
260
|
+
* consumer searching for "Aiden Phoebus" could not match the one cell he is
|
|
261
|
+
* actually in. These are the names that make him findable.
|
|
262
|
+
*/
|
|
263
|
+
individualParticipants?: {
|
|
264
|
+
participantId?: string;
|
|
265
|
+
participantName?: string;
|
|
266
|
+
}[];
|
|
241
267
|
}
|
|
242
268
|
/**
|
|
243
269
|
* Flat data shape the cell renderer consumes — mapped from factory matchUp objects.
|
|
@@ -10,3 +10,8 @@ export type FormatOptions = {
|
|
|
10
10
|
* Example: "6464" with matchUpFormat 'SET3-S:6/TB7' becomes "6-4 6-4"
|
|
11
11
|
*/
|
|
12
12
|
export declare function formatScoreString(digits: string, options: FormatOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* The number of sets a format names. `matchUpFormatCode.parse` puts `bestOf` and `exactly` at the top level
|
|
15
|
+
* of the parsed format, never inside `setFormat`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setCountOf(parsedFormat: any): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SideNumber } from './logic/scoreEntryState';
|
|
2
|
+
/**
|
|
3
|
+
* The LOWER participant's row — where entry begins, in every approach.
|
|
4
|
+
*
|
|
5
|
+
* CA, 2026-09-28: *"the dynamic sets when launched should always initially give focus to the lower
|
|
6
|
+
* row; that way e.g. 3 is lower row and shift+3 is upper row, which feels natural ... each subsequent
|
|
7
|
+
* entry column should start on the bottom cell."*
|
|
8
|
+
*
|
|
9
|
+
* Named rather than written as a bare `2` at each site: it is one decision, and the shifted case is
|
|
10
|
+
* derived from it (`otherSide`) so the two can never be set independently and disagree.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ENTRY_SIDE: SideNumber;
|
|
13
|
+
/** The side a SHIFTED keystroke writes to — the upper row, by construction. */
|
|
14
|
+
export declare const SHIFTED_SIDE: SideNumber;
|
|
15
|
+
export declare function otherSide(sideNumber: SideNumber): SideNumber;
|
|
16
|
+
/**
|
|
17
|
+
* The digit a key press means, or `undefined` for anything that is not a digit key.
|
|
18
|
+
*
|
|
19
|
+
* Both the number row and the numpad, because an operator entering scores at speed uses whichever is
|
|
20
|
+
* under their hand.
|
|
21
|
+
*/
|
|
22
|
+
export declare function digitFromCode(code: string): number | undefined;
|
|
23
|
+
/** Whether a key event carries a modifier that means it belongs to the browser or the OS. */
|
|
24
|
+
export declare function hasCommandModifier(event: KeyboardEvent): boolean;
|
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
import { SetScore } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Set format information returned by matchUpFormatCode.parse()
|
|
4
|
+
*
|
|
5
|
+
* ── `NoAD`, and why the casing is load-bearing ──
|
|
6
|
+
*
|
|
7
|
+
* This type is a hand-written MIRROR of what `matchUpFormatCode.parse` emits, and it named the
|
|
8
|
+
* no-advantage flag `noAd` while the factory emits **`NoAD`**. Measured 2026-09-28:
|
|
9
|
+
* `parse('SET1-S:TB10NOAD').setFormat.tiebreakSet` is `{ tiebreakTo: 10, NoAD: true }`, so every read
|
|
10
|
+
* of `.noAd` in this module answered `undefined` for every format — and the TYPE is what made those
|
|
11
|
+
* reads look correct.
|
|
12
|
+
*
|
|
13
|
+
* Nothing threw and nothing logged; no-advantage scoring was simply ignored. Two measured
|
|
14
|
+
* consequences, both live in the shipping dialog: a low of 6 in a `TB7NOAD` completed to **8** instead
|
|
15
|
+
* of 7, and a low of 5 in `S:6NOAD` completed to **7** instead of 6 — a 7-5 in a format where first to
|
|
16
|
+
* six wins.
|
|
17
|
+
*
|
|
18
|
+
* Every OTHER reader of this flag in the package had it right — `freeScore.ts`, `matchUpFormat.ts`,
|
|
19
|
+
* `matchUpFormatLogic.ts` and the format picker all read `.NoAD`. This one type was the outlier, which
|
|
20
|
+
* is the shape the architectural standards call mock divergence: a hand-written mirror that renames a
|
|
21
|
+
* field and then certifies the rename.
|
|
4
22
|
*/
|
|
5
23
|
export type SetFormat = {
|
|
6
24
|
setTo?: number;
|
|
7
25
|
tiebreakAt?: number;
|
|
26
|
+
/** No-advantage on the SET — with no tiebreak, the set ends at `setTo` without a two-game margin. */
|
|
27
|
+
NoAD?: boolean;
|
|
8
28
|
tiebreakFormat?: {
|
|
9
29
|
tiebreakTo?: number;
|
|
10
|
-
|
|
30
|
+
NoAD?: boolean;
|
|
11
31
|
};
|
|
12
32
|
tiebreakSet?: {
|
|
13
33
|
tiebreakTo?: number;
|
|
14
|
-
|
|
34
|
+
NoAD?: boolean;
|
|
15
35
|
};
|
|
16
36
|
timed?: boolean;
|
|
17
37
|
minutes?: number;
|
|
@@ -25,6 +45,7 @@ export type SetFormat = {
|
|
|
25
45
|
export type MatchUpConfig = {
|
|
26
46
|
bestOf: number;
|
|
27
47
|
exactly?: number;
|
|
48
|
+
aggregate?: boolean;
|
|
28
49
|
setFormat?: SetFormat;
|
|
29
50
|
finalSetFormat?: SetFormat;
|
|
30
51
|
};
|
|
@@ -37,10 +58,7 @@ export type SmartComplementResult = {
|
|
|
37
58
|
shouldApply: boolean;
|
|
38
59
|
reason?: string;
|
|
39
60
|
};
|
|
40
|
-
|
|
41
|
-
* Get the format for a specific set index
|
|
42
|
-
* Uses finalSetFormat for deciding set if available
|
|
43
|
-
*/
|
|
61
|
+
export declare function matchUpConfigFor(matchUpFormat?: string): MatchUpConfig;
|
|
44
62
|
export declare function getSetFormatForIndex(setIndex: number, config: MatchUpConfig): SetFormat | undefined;
|
|
45
63
|
/**
|
|
46
64
|
* Check if a set is tiebreak-only (e.g., TB10)
|
|
@@ -80,12 +98,12 @@ export declare function getSetWinner(setIndex: number, scores: {
|
|
|
80
98
|
/**
|
|
81
99
|
* Determine if match is complete based on sets won
|
|
82
100
|
*/
|
|
83
|
-
export declare function isMatchComplete(sets: SetScore[],
|
|
101
|
+
export declare function isMatchComplete(sets: SetScore[], config: MatchUpConfig): boolean;
|
|
84
102
|
/**
|
|
85
103
|
* Get the match winner based on sets won
|
|
86
104
|
* Returns undefined if match is not complete
|
|
87
105
|
*/
|
|
88
|
-
export declare function getMatchWinner(sets: SetScore[],
|
|
106
|
+
export declare function getMatchWinner(sets: SetScore[], config: MatchUpConfig): 1 | 2 | undefined;
|
|
89
107
|
/**
|
|
90
108
|
* Calculate the complement score for smart complement entry
|
|
91
109
|
* Returns null if no predictable complement exists
|
|
@@ -24,27 +24,64 @@
|
|
|
24
24
|
export declare const NEITHER_SIDE = "NEITHER";
|
|
25
25
|
/** Shown when an irregular ending has been chosen but the winner question is unanswered. */
|
|
26
26
|
export declare const WINNER_REQUIRED_ERROR = "Select a winner";
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Every irregular ending a set-entry approach offers, in display order.
|
|
29
|
+
*
|
|
30
|
+
* ── Why ten, when this list held six ──
|
|
31
|
+
*
|
|
32
|
+
* It held six because those are exactly the keys the factory's scoring policy can refine with
|
|
33
|
+
* `matchUpStatusCodes`, the reasoning being that a status with no code group cannot carry a reason.
|
|
34
|
+
* That is true but it was the wrong list to derive a VOCABULARY from, and the cost was measurable:
|
|
35
|
+
* `freeScoreApproach` has always parsed ten, so four statuses were typeable in one approach and
|
|
36
|
+
* unreachable in the other three. Rotating between approaches could therefore show an ending that
|
|
37
|
+
* could not be re-selected. The four additions (SUSPENDED, IN_PROGRESS, AWAITING_RESULT,
|
|
38
|
+
* DEAD_RUBBER) simply carry no reason, which is the designed quiet state, not a defect.
|
|
39
|
+
*
|
|
40
|
+
* Two things checked while widening, recorded because neither is visible from here:
|
|
41
|
+
*
|
|
42
|
+
* - `WITHDRAWN` is NOT a factory matchUpStatus (`matchUpStatusConstants.WITHDRAWN` will not even
|
|
43
|
+
* compile), yet the USTA policy keys a seventh `matchUpStatusCodes` group with it, from
|
|
44
|
+
* `entryStatusConstants`. This comment previously called that "a code group no status can reach"
|
|
45
|
+
* and a factory problem. Both halves were wrong, and this is settled, twice over:
|
|
46
|
+
*
|
|
47
|
+
* CA, 2026-09-27: "WITHDRAWN is a statusCode on a WALKOVER... WALKOVER (withdrawn injured or
|
|
48
|
+
* withdrawn ill)" — a withdrawal is expressed AS a walkover, and the WALKOVER group already ships
|
|
49
|
+
* that expression as `W5` / "Wo/Withdrawn". CA, 2026-09-19: the WALKOVER group records "a match
|
|
50
|
+
* that did not happen because someone withdrew", a RESULT; the WITHDRAWN group records "the
|
|
51
|
+
* withdrawal itself", an ENTRY action belonging to the entries UI. Same reasons, two events, two
|
|
52
|
+
* surfaces — which is why the groups are label-for-label parallel and must NOT be merged.
|
|
53
|
+
* `statusCodes.test.ts` holds that, including the measurement that refutes merging them.
|
|
54
|
+
* - The ten partition PERFECTLY onto the factory's own classification: three directing, seven
|
|
55
|
+
* non-directing, with nothing in both and nothing in neither. The `endingPartition` test asserts
|
|
56
|
+
* this rather than trusting it.
|
|
57
|
+
*
|
|
58
|
+
* Ordered so the three that name a winner come first, then the three an operator reaches for most
|
|
59
|
+
* often mid-tournament, then the remainder.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SELECTABLE_ENDINGS: string[];
|
|
62
|
+
/**
|
|
63
|
+
* The statuses whose selection opens the winner question at all.
|
|
64
|
+
*
|
|
65
|
+
* DERIVED from the factory's `directingMatchUpStatuses` rather than hand-listed, because "does a
|
|
66
|
+
* winner advance out of this status" is the factory's question to answer and it already answers it.
|
|
67
|
+
* Hand-adding a status to two sets is precisely where a mistake hides, and the eleventh ending
|
|
68
|
+
* should classify itself rather than wait for someone to remember both places.
|
|
69
|
+
*/
|
|
28
70
|
export declare const WINNER_REQUIRING_STATUSES: Set<string>;
|
|
29
71
|
/**
|
|
30
72
|
* Endings that resolve nobody: the match did not produce a result, so there is no winner to name
|
|
31
|
-
* and no winner question to answer.
|
|
32
|
-
* (`nonDirectingMatchUpStatuses`) — nothing advances out of them.
|
|
73
|
+
* and no winner question to answer. Nothing advances out of them.
|
|
33
74
|
*
|
|
34
75
|
* These are NOT a smaller version of the winner-requiring endings. Asking "who won an abandoned
|
|
35
76
|
* match" is not a question with a missing answer; it is not a question. So unlike a walkover with
|
|
36
77
|
* no winner selection, one of these is valid the moment it is chosen.
|
|
37
|
-
*/
|
|
38
|
-
export declare const NON_DIRECTING_ENDINGS: Set<string>;
|
|
39
|
-
/**
|
|
40
|
-
* Every irregular ending a set-entry approach offers, in display order.
|
|
41
78
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
79
|
+
* Also derived, and the complement of `WINNER_REQUIRING_STATUSES` over `SELECTABLE_ENDINGS` — but
|
|
80
|
+
* computed independently from `nonDirectingMatchUpStatuses` rather than as `!winnerRequiring`, so
|
|
81
|
+
* that a status the factory somehow classifies as NEITHER shows up as a test failure instead of
|
|
82
|
+
* being silently swept in here.
|
|
46
83
|
*/
|
|
47
|
-
export declare const
|
|
84
|
+
export declare const NON_DIRECTING_ENDINGS: Set<string>;
|
|
48
85
|
/** Whether choosing this ending obliges the operator to answer the winner question. */
|
|
49
86
|
export declare function requiresWinner(selectedOutcome: string | undefined): boolean;
|
|
50
87
|
/**
|
|
@@ -57,12 +94,52 @@ export declare function requiresWinner(selectedOutcome: string | undefined): boo
|
|
|
57
94
|
* score is real. The same goes for `RETIRED` — a retirement keeps whatever was played, and needs no
|
|
58
95
|
* completed set to be a valid result.
|
|
59
96
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
97
|
+
* ── CANCELLED and DEAD_RUBBER are here now, and that is a CLIENT policy ──
|
|
98
|
+
*
|
|
99
|
+
* They were held out on the stated grounds that they were "unreachable from the set-entry approaches
|
|
100
|
+
* (only freeScore parses them)", with a note to revisit when the vocabulary widened. Two corrections
|
|
101
|
+
* to that, found on widening it:
|
|
102
|
+
*
|
|
103
|
+
* 1. CANCELLED was never unreachable — it has been in `SELECTABLE_ENDINGS` all along and renders as
|
|
104
|
+
* a radio in both Dynamic Sets and Dial Pad. Only DEAD_RUBBER was genuinely out of reach.
|
|
105
|
+
* 2. The rule was not absent, it was TRIPLICATED, and the three copies disagreed:
|
|
106
|
+
*
|
|
107
|
+
* NO_SCORE_STATUSES WALKOVER DOUBLE_WALKOVER
|
|
108
|
+
* validateScore WALKOVER CANCELLED DEAD_RUBBER
|
|
109
|
+
* freeScore display WALKOVER CANCELLED DEAD_RUBBER
|
|
110
|
+
*
|
|
111
|
+
* No copy held all four, and the two that agreed with each other both omitted DOUBLE_WALKOVER.
|
|
112
|
+
* This set is now the single one, and CA settled its contents (2026-09-27): selecting Cancelled
|
|
113
|
+
* or Dead Rubber clears any partial score present.
|
|
114
|
+
*
|
|
115
|
+
* Note what that means and does not mean. The FACTORY blanks scores for `{WALKOVER,
|
|
116
|
+
* DOUBLE_WALKOVER}` only (`modifyMatchUpScore.ts:236`, verified) — `removeScore` is its separate
|
|
117
|
+
* lever for anything else. So nothing server-side enforces the clearing of CANCELLED or DEAD_RUBBER;
|
|
118
|
+
* the client simply never submits a score for them, which reaches the same place. Do not "correct"
|
|
119
|
+
* this set toward the factory's: it is deliberately wider, and the widening is the policy.
|
|
120
|
+
*
|
|
121
|
+
* Still NOT here, and each for a reason worth keeping: `DEFAULTED` / `DOUBLE_DEFAULT`, because a
|
|
122
|
+
* player can default part-way through a match that was genuinely played; `RETIRED`, which keeps
|
|
123
|
+
* whatever was played; and ABANDONED / INCOMPLETE / SUSPENDED, where the partial score is the entire
|
|
124
|
+
* content of the result — "6-4 3-2, suspended" says when it was suspended.
|
|
64
125
|
*/
|
|
65
126
|
export declare const NO_SCORE_STATUSES: Set<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Endings a typed score legitimately survives alongside.
|
|
129
|
+
*
|
|
130
|
+
* The Dial Pad clears the selected ending when a digit is typed — the operator is starting over —
|
|
131
|
+
* and must not do that for an ending whose whole meaning includes a partial score. It decided this
|
|
132
|
+
* by asking `NON_DIRECTING_ENDINGS`, which is a DIFFERENT question: "does anyone advance" is not
|
|
133
|
+
* "is there a score". The two happened to coincide across the old six. They do not across the ten:
|
|
134
|
+
* DEAD_RUBBER and CANCELLED are non-directing yet carry no score, so the old gate would have
|
|
135
|
+
* preserved an ending that had just discarded the digits being typed.
|
|
136
|
+
*
|
|
137
|
+
* So it is stated as its own predicate over both axes, and the `endingPartition` test pins the case
|
|
138
|
+
* that made the difference.
|
|
139
|
+
*/
|
|
140
|
+
export declare const SCORE_PRESERVING_ENDINGS: Set<string>;
|
|
141
|
+
/** Whether a typed score should be kept when this ending is selected. */
|
|
142
|
+
export declare function coexistsWithScore(selectedOutcome: string | undefined): boolean;
|
|
66
143
|
/**
|
|
67
144
|
* Whether this ending means "no score exists", so score validation must be skipped rather than run
|
|
68
145
|
* and then overridden.
|
|
@@ -139,3 +216,66 @@ export declare function resolveIrregularEnding(params: {
|
|
|
139
216
|
* `error: 'At least one set is required'` was the symptom of exactly that missing check.
|
|
140
217
|
*/
|
|
141
218
|
export declare function applyIrregularEndingToValidation(validation: any, selectedOutcome: string | undefined, winnerSelection: WinnerSelection): IrregularEndingResolution;
|
|
219
|
+
/**
|
|
220
|
+
* What the controls should show for a matchUp that already carries a result.
|
|
221
|
+
*
|
|
222
|
+
* The documented INVERSE of `resolveIrregularEnding`. That function was hoisted here because three
|
|
223
|
+
* approaches each carried their own copy of the forward rule and disagreed; the inverse was left
|
|
224
|
+
* behind in each of them and diverged the same way. Measured before this existed:
|
|
225
|
+
*
|
|
226
|
+
* - `dynamicSetsApproach` restored all six endings and both double exits;
|
|
227
|
+
* - `dialPadApproach` restored only RETIRED / WALKOVER / DEFAULTED, so a saved ABANDONED,
|
|
228
|
+
* CANCELLED or INCOMPLETE re-opened reading as COMPLETED — and re-submitting silently replaced
|
|
229
|
+
* a non-directing status;
|
|
230
|
+
* - `freeScoreApproach` used a third mechanism entirely;
|
|
231
|
+
* - `inlineScoringApproach` discarded the saved result by construction.
|
|
232
|
+
*
|
|
233
|
+
* Round-tripping is the property that matters, and it is the one a test can hold: for every status
|
|
234
|
+
* in `SELECTABLE_ENDINGS`, `resolveIrregularEnding(hydrateIrregularEnding(m))` must return the
|
|
235
|
+
* status `m` was saved with.
|
|
236
|
+
*
|
|
237
|
+
* ── What each branch means ──
|
|
238
|
+
*
|
|
239
|
+
* A **double exit** is stored as its own status and carries no `winningSide`, so it inverts to the
|
|
240
|
+
* base ending plus an explicit `NEITHER_SIDE` — the tri-state's third value, recovered rather than
|
|
241
|
+
* guessed. A **non-directing** ending (ABANDONED, CANCELLED, INCOMPLETE) resolves nobody, so its
|
|
242
|
+
* winner is `undefined` and must stay that way; reading a stray `winningSide` back onto one would
|
|
243
|
+
* re-introduce the fail-open shape this module exists to prevent. Anything else — COMPLETED,
|
|
244
|
+
* TO_BE_PLAYED, IN_PROGRESS, a status this vocabulary does not offer, or no matchUp at all —
|
|
245
|
+
* inverts to COMPLETED with no winner, which is the inert state the controls open in.
|
|
246
|
+
*
|
|
247
|
+
* Deliberately NOT inferred: a `winningSide` present on a status that requires one is trusted, but a
|
|
248
|
+
* MISSING one is returned as `undefined` rather than defaulted. An unanswered winner is the
|
|
249
|
+
* fail-closed state (`WINNER_REQUIRED_ERROR`), and it must survive a save/reopen cycle as unanswered.
|
|
250
|
+
*/
|
|
251
|
+
export declare function hydrateIrregularEnding(matchUp?: {
|
|
252
|
+
matchUpStatus?: string;
|
|
253
|
+
winningSide?: number;
|
|
254
|
+
}): {
|
|
255
|
+
selectedOutcome: string;
|
|
256
|
+
winnerSelection: WinnerSelection;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Display labels for every selectable ending, with locale overrides applied.
|
|
260
|
+
*
|
|
261
|
+
* Shared because it was not: Dynamic Sets and Dial Pad each kept their own map covering only the
|
|
262
|
+
* statuses they offered, and Dynamic Sets read it as `ENDING_LABELS[value]` with NO fallback — so
|
|
263
|
+
* widening `SELECTABLE_ENDINGS` without this would have rendered four radios labelled `undefined`.
|
|
264
|
+
* One map means an eleventh ending is either labelled or caught by `everySelectableEndingIsLabelled`.
|
|
265
|
+
*
|
|
266
|
+
* The fallback is the raw status rather than a blank, so an unlabelled ending is visibly wrong in the
|
|
267
|
+
* UI rather than invisibly missing.
|
|
268
|
+
*/
|
|
269
|
+
export type EndingLabelOverrides = {
|
|
270
|
+
retired?: string;
|
|
271
|
+
walkover?: string;
|
|
272
|
+
defaulted?: string;
|
|
273
|
+
inProgress?: string;
|
|
274
|
+
awaitingResult?: string;
|
|
275
|
+
suspended?: string;
|
|
276
|
+
abandoned?: string;
|
|
277
|
+
cancelled?: string;
|
|
278
|
+
incomplete?: string;
|
|
279
|
+
deadRubber?: string;
|
|
280
|
+
};
|
|
281
|
+
export declare function endingLabels(labels?: EndingLabelOverrides): Record<string, string>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ScoreOutcome } from '../types';
|
|
2
|
+
/** The fields a projection may replace. Everything else on the matchUp is carried through as-is. */
|
|
3
|
+
type ProjectableMatchUp = {
|
|
4
|
+
score?: any;
|
|
5
|
+
matchUpStatus?: string;
|
|
6
|
+
winningSide?: number;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The matchUp the next approach should hydrate from.
|
|
11
|
+
*
|
|
12
|
+
* Returns the original when there is nothing in progress to carry — the ordinary case on first open,
|
|
13
|
+
* where the saved matchUp IS the truth.
|
|
14
|
+
*
|
|
15
|
+
* ── `winningSide` takes no fallback, and that is the whole care of this function ──
|
|
16
|
+
*
|
|
17
|
+
* It is read from the outcome and never from the saved matchUp. If the operator has cleared the
|
|
18
|
+
* score, the outcome carries no winner, and falling back to the stored one would put back a winner
|
|
19
|
+
* they had just removed — an unanswered question read as a particular answer, which is the fail-open
|
|
20
|
+
* shape (Mentat architectural standard A3) that `irregularEnding.ts` exists to prevent, one field
|
|
21
|
+
* over. `score` and `matchUpStatus` DO fall back, because an outcome that asserts neither is not
|
|
22
|
+
* claiming the match has no score or no status; it is simply not speaking about them.
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectOutcomeOntoMatchUp<T extends ProjectableMatchUp>(matchUp: T, outcome?: ScoreOutcome | null): T;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { hydrateScoreEntryState, ScoreEntryState, SideNumber } from './scoreEntryState';
|
|
2
|
+
import { MatchUpConfig } from './dynamicSetsLogic';
|
|
3
|
+
import { SetScore } from '../types';
|
|
4
|
+
/** The three ways of typing a score. The dialog's own union of the same three names is re-pointed here in S5. */
|
|
5
|
+
export type ScoreEntryApproach = 'dynamicSets' | 'freeScore' | 'dialPad';
|
|
6
|
+
/**
|
|
7
|
+
* One set as ENTERED, cell by cell. Any cell may be empty, which is how a half-entered set is kept.
|
|
8
|
+
*
|
|
9
|
+
* `side1` / `side2` are the set's principal score — games, or the POINTS of a tiebreak-only set, exactly
|
|
10
|
+
* as the regions' cells hold them. `tiebreak1` / `tiebreak2` are the points that decided an ordinary set
|
|
11
|
+
* that went to a tiebreak.
|
|
12
|
+
*/
|
|
13
|
+
export type SetEntry = {
|
|
14
|
+
side1?: number;
|
|
15
|
+
side2?: number;
|
|
16
|
+
tiebreak1?: number;
|
|
17
|
+
tiebreak2?: number;
|
|
18
|
+
};
|
|
19
|
+
/** Which cell of which set an intent is aimed at. A tiebreak-only set has no `tiebreak` cells. */
|
|
20
|
+
export type CellRef = {
|
|
21
|
+
setIndex: number;
|
|
22
|
+
side: SideNumber;
|
|
23
|
+
kind: 'games' | 'tiebreak';
|
|
24
|
+
};
|
|
25
|
+
export type ScoreEntryModel = {
|
|
26
|
+
matchUpFormat: string;
|
|
27
|
+
approach: ScoreEntryApproach;
|
|
28
|
+
/** Exactly the set count the format plays, every entry possibly empty. */
|
|
29
|
+
sets: readonly SetEntry[];
|
|
30
|
+
/** The existing pure ending engine, folded in unchanged. */
|
|
31
|
+
ending: ScoreEntryState;
|
|
32
|
+
};
|
|
33
|
+
/** The ways an ending is chosen, one per transition in `scoreEntryState.ts`. */
|
|
34
|
+
export type EndingIntent = {
|
|
35
|
+
kind: 'side';
|
|
36
|
+
sideNumber: SideNumber;
|
|
37
|
+
status: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'match';
|
|
40
|
+
status: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'bothSidesOut';
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'reasonCode';
|
|
45
|
+
code: string | undefined;
|
|
46
|
+
};
|
|
47
|
+
export type CreateScoreEntryModelParams = {
|
|
48
|
+
matchUpFormat?: string;
|
|
49
|
+
approach?: ScoreEntryApproach;
|
|
50
|
+
/** A saved score to open with, in factory shape. */
|
|
51
|
+
sets?: SetScore[];
|
|
52
|
+
/** A saved matchUp whose outcome hydrates the ending — see `hydrateScoreEntryState`. */
|
|
53
|
+
matchUp?: Parameters<typeof hydrateScoreEntryState>[0];
|
|
54
|
+
};
|
|
55
|
+
/** What a model with no format opens under. The same fallback `freeScoreRegion` and `matchUpConfigFor` use. */
|
|
56
|
+
export declare const DEFAULT_MATCH_UP_FORMAT = "SET3-S:6/TB7";
|
|
57
|
+
export declare function createScoreEntryModel(params?: CreateScoreEntryModelParams): ScoreEntryModel;
|
|
58
|
+
export type TypeDigitIntent = {
|
|
59
|
+
cell: CellRef;
|
|
60
|
+
digit: number;
|
|
61
|
+
/**
|
|
62
|
+
* Derive the OTHER side from what was typed, where the format makes that a single answer.
|
|
63
|
+
*
|
|
64
|
+
* For a games cell this is the smart complement — a `4` typed into an EMPTY cell writes `6` opposite,
|
|
65
|
+
* over whatever the other cell held: the inference is about the pair. Whether to offer it again for
|
|
66
|
+
* the same set is the caller's policy. For an ordinary tiebreak it derives the
|
|
67
|
+
* winner's points from the loser's, and fires only from the games LOSER's cell, because a winner's 7
|
|
68
|
+
* could have beaten anything. For a tiebreak-only set it derives the other side from the one typed —
|
|
69
|
+
* CA's rule for the Dial Pad, which always asks; Dynamic Sets asks while its Smart toggle is on.
|
|
70
|
+
*/
|
|
71
|
+
complement?: boolean;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Append a digit to a cell, as a keystroke does.
|
|
75
|
+
*
|
|
76
|
+
* Refused — the same model comes back — when the digit could not become a legal score: past the
|
|
77
|
+
* ceiling for a games cell, or past two digits where no ceiling exists. Refusal is what lets a keypad
|
|
78
|
+
* move on to the next cell rather than the model guessing.
|
|
79
|
+
*
|
|
80
|
+
* The ceiling is `getMaxSetScore`'s, given the OPPONENT's games where they are in: 7 in a set to six,
|
|
81
|
+
* tightened to 6 for a side facing 3 — 7 is reachable only through a tiebreak at six-all — and left at
|
|
82
|
+
* 7 for a side facing 5, since 7-5 needs no tiebreak. That last answer is `#5043(factory)`, merged
|
|
83
|
+
* 2026-09-30; this module is written to the fixed factory, and a `7` typed against a `5` is pinned.
|
|
84
|
+
*/
|
|
85
|
+
export declare function typeDigit(model: ScoreEntryModel, intent: TypeDigitIntent): ScoreEntryModel;
|
|
86
|
+
/**
|
|
87
|
+
* Write a whole value into a cell, as setting an input's value does. `undefined` empties it.
|
|
88
|
+
*
|
|
89
|
+
* Nothing is clamped or derived: the value recorded is the value given, and whether it is legal is
|
|
90
|
+
* `error`'s question. A keystroke goes through `typeDigit`, which is where the derivations live.
|
|
91
|
+
*/
|
|
92
|
+
export declare function setCell(model: ScoreEntryModel, cell: CellRef, value: number | undefined): ScoreEntryModel;
|
|
93
|
+
export declare function clearCell(model: ScoreEntryModel, cell: CellRef): ScoreEntryModel;
|
|
94
|
+
/**
|
|
95
|
+
* Empty every set and keep the ending: what a score-clearing ending does to the cells.
|
|
96
|
+
*
|
|
97
|
+
* CA, 2026-09-30: *"when I enter set score(s) and then select (Walkover) the set score(s) should
|
|
98
|
+
* clear."* The card calls this when the chosen ending carries no score, having first read `scoreString`
|
|
99
|
+
* for the band. Final, like `[Clear]`: un-choosing the walkover does not bring the score back, because a
|
|
100
|
+
* remembered score that silently reappears on an approach switch was the worse bug.
|
|
101
|
+
*/
|
|
102
|
+
export declare function clearScore(model: ScoreEntryModel): ScoreEntryModel;
|
|
103
|
+
/** Empty every set and un-choose the ending: the card's `[Clear]`. Format and approach stay. */
|
|
104
|
+
export declare function clearAll(model: ScoreEntryModel): ScoreEntryModel;
|
|
105
|
+
/**
|
|
106
|
+
* Choose (or un-choose) an ending, delegating to `scoreEntryState.ts` — every control there is a
|
|
107
|
+
* toggle, and that is preserved.
|
|
108
|
+
*
|
|
109
|
+
* The SETS are left exactly as they are, even for an ending that carries no score. The card today
|
|
110
|
+
* clears the region on a walkover and keeps the string aside for the band; here the selectors suppress
|
|
111
|
+
* the score while such an ending is chosen (`scoreString` answers nothing, `discardedScore` names what
|
|
112
|
+
* was typed) and it comes back if the ending is un-chosen. Keeping the keystrokes is the point of the
|
|
113
|
+
* model; whether the card should ALSO clear explicitly is S5's decision, and `clearAll` is there for it.
|
|
114
|
+
*/
|
|
115
|
+
export declare function chooseEnding(model: ScoreEntryModel, intent: EndingIntent): ScoreEntryModel;
|
|
116
|
+
/**
|
|
117
|
+
* Move to another format, keeping what the new format has not invalidated.
|
|
118
|
+
*
|
|
119
|
+
* `retainScoreForFormat` decides, given both formats: a set whose position keeps the same rule survives
|
|
120
|
+
* finished or not, a set whose rule changed must stand as a complete legal set, and everything after the
|
|
121
|
+
* first casualty goes with it. It keeps a PREFIX, so the entries are cut to its length rather than
|
|
122
|
+
* rebuilt from its output — the operator's cells survive untouched. The array is then resized to the
|
|
123
|
+
* new format's set count.
|
|
124
|
+
*/
|
|
125
|
+
export declare function changeFormat(model: ScoreEntryModel, matchUpFormat: string): ScoreEntryModel;
|
|
126
|
+
/** Change how the score is typed. A no-op on the score itself: the sets are the sets whichever region shows them. */
|
|
127
|
+
export declare function switchApproach(model: ScoreEntryModel, approach: ScoreEntryApproach): ScoreEntryModel;
|
|
128
|
+
/**
|
|
129
|
+
* The index of the set that decides the match, or `undefined` while nothing does.
|
|
130
|
+
*
|
|
131
|
+
* Asked of the factory prefix by prefix, so that a decision reached at set 2 is found even when a stale
|
|
132
|
+
* set 3 would make the whole array read as undecided — measured: `analyzeMatchUp` on `6-2 6-2 6-3` with
|
|
133
|
+
* three winning sides answers NO winner, because three is not `setsToWin`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function decidingIndex(sets: readonly SetEntry[], config: MatchUpConfig): number | undefined;
|
|
136
|
+
/** The sets with BOTH principal cells in, as the factory reads them, positioned by `setNumber`. */
|
|
137
|
+
export declare function enteredFactorySets(sets: readonly SetEntry[], config: MatchUpConfig): SetScore[];
|
|
138
|
+
/**
|
|
139
|
+
* One entry in the factory's shape.
|
|
140
|
+
*
|
|
141
|
+
* `buildSetScore` derives the winning side and the tiebreak pair from the loser's points, which is the
|
|
142
|
+
* contract this module keeps: the lower of two tiebreak cells is the loser's. A cell that is empty stays
|
|
143
|
+
* empty in the output — it is not read as 0, which is what let a half-entered set claim `6-0`.
|
|
144
|
+
*
|
|
145
|
+
* A tiebreak-only set is built here rather than by `buildSetScore`. When this was written that function's
|
|
146
|
+
* winner rule was a hand-rolled "both sides above zero and unequal" (a `3-1` won, a `10-0` not); it asks
|
|
147
|
+
* the factory now too, and this path simply never left. The set's winner is `analyzeSet`'s, given the
|
|
148
|
+
* points AS points — `getSetWinner` would hand them over as games, which a tiebreak-only format has no
|
|
149
|
+
* rule for.
|
|
150
|
+
*/
|
|
151
|
+
export declare function toFactorySet(entry: SetEntry, index: number, config: MatchUpConfig): SetScore;
|
|
152
|
+
/**
|
|
153
|
+
* Replace every set from a factory-shaped array: the Free Score boundary, where TEXT becomes sets.
|
|
154
|
+
*
|
|
155
|
+
* Free Score keeps the text as the field's own value while it is being typed — a half-typed `6-4 re` is
|
|
156
|
+
* nothing the model can represent — and commits here at the points where the text parses to sets. The
|
|
157
|
+
* same seeding and the same invariants as opening a saved score: a set past the decider is trimmed.
|
|
158
|
+
* The same model comes back when nothing changed, so a keystroke inside a word is not a transition.
|
|
159
|
+
*/
|
|
160
|
+
export declare function replaceSets(model: ScoreEntryModel, sets: SetScore[]): ScoreEntryModel;
|
|
161
|
+
export declare function readCell(entry: SetEntry, cell: CellRef): number | undefined;
|
|
162
|
+
/** The side that lost the set on games, or `undefined` while the games are level or not both in. */
|
|
163
|
+
export declare function gamesLoser(entry: SetEntry): SideNumber | undefined;
|
|
164
|
+
/** Whether both principal cells are in — the regions' `bothEntered`. */
|
|
165
|
+
export declare function isEntered(entry: SetEntry): boolean;
|
|
166
|
+
/** Whether the set holds anything at all, in any cell. */
|
|
167
|
+
export declare function isEmptyEntry(entry: SetEntry): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* How many sets the format can hold: every set of an `exactly` format, otherwise the best-of. An
|
|
170
|
+
* aggregate format with a final tiebreak holds one more, its sudden-death decider, which is set N + 1
|
|
171
|
+
* and never one of the N (factory #5166; CA, 2026-10-04). It opens only on a level total.
|
|
172
|
+
*/
|
|
173
|
+
export declare function setCountOf(config: MatchUpConfig): number;
|
|
174
|
+
/** The set number of an aggregate format's final-tiebreak decider, or `undefined` when it has none. */
|
|
175
|
+
export declare function deciderSetNumberOf(config: MatchUpConfig): number | undefined;
|