@pickleball/expo-sdk 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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/app.plugin.js +6 -0
- package/dist/chunk-VHLUJQG5.js +17 -0
- package/dist/index.cjs +6284 -0
- package/dist/index.d.cts +1436 -0
- package/dist/index.d.ts +1436 -0
- package/dist/index.js +6186 -0
- package/dist/plugin.cjs +153 -0
- package/dist/plugin.d.cts +21 -0
- package/dist/plugin.d.ts +21 -0
- package/dist/plugin.js +131 -0
- package/dist/version.cjs +35 -0
- package/dist/version.d.cts +4 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +6 -0
- package/package.json +126 -0
- package/src/contracts.ts +211 -0
- package/src/engine.ts +1332 -0
- package/src/errors.ts +128 -0
- package/src/http-session-provider.ts +259 -0
- package/src/index.ts +16 -0
- package/src/livekit-room-adapter.ts +353 -0
- package/src/livestream-screen.tsx +980 -0
- package/src/match/contracts.ts +137 -0
- package/src/match/engine.ts +702 -0
- package/src/match/persistence/default-store.native.ts +20 -0
- package/src/match/persistence/default-store.ts +11 -0
- package/src/match/persistence/index.ts +6 -0
- package/src/match/persistence/memory-driver.ts +255 -0
- package/src/match/persistence/migrations.ts +40 -0
- package/src/match/persistence/sqlite-driver.ts +348 -0
- package/src/match/persistence/store.ts +497 -0
- package/src/match/persistence/types.ts +98 -0
- package/src/match/provider.tsx +728 -0
- package/src/match/remote/deduplicator.ts +54 -0
- package/src/match/remote/encoding.ts +12 -0
- package/src/match/remote/hid-gesture.ts +61 -0
- package/src/match/remote/index.ts +5 -0
- package/src/match/remote/native-service.ts +62 -0
- package/src/match/remote/parser.ts +134 -0
- package/src/match/remote/types.ts +116 -0
- package/src/match/replay.ts +102 -0
- package/src/match/state.ts +72 -0
- package/src/match/validation.ts +375 -0
- package/src/match/voice/assets/court-left.wav +0 -0
- package/src/match/voice/assets/court-right.wav +0 -0
- package/src/match/voice/assets/court-switch.wav +0 -0
- package/src/match/voice/assets/game-point.wav +0 -0
- package/src/match/voice/assets/game-won.wav +0 -0
- package/src/match/voice/assets/match-won.wav +0 -0
- package/src/match/voice/assets/number-0.wav +0 -0
- package/src/match/voice/assets/number-1.wav +0 -0
- package/src/match/voice/assets/number-10.wav +0 -0
- package/src/match/voice/assets/number-11.wav +0 -0
- package/src/match/voice/assets/number-12.wav +0 -0
- package/src/match/voice/assets/number-13.wav +0 -0
- package/src/match/voice/assets/number-14.wav +0 -0
- package/src/match/voice/assets/number-15.wav +0 -0
- package/src/match/voice/assets/number-16.wav +0 -0
- package/src/match/voice/assets/number-17.wav +0 -0
- package/src/match/voice/assets/number-18.wav +0 -0
- package/src/match/voice/assets/number-19.wav +0 -0
- package/src/match/voice/assets/number-2.wav +0 -0
- package/src/match/voice/assets/number-20.wav +0 -0
- package/src/match/voice/assets/number-3.wav +0 -0
- package/src/match/voice/assets/number-30.wav +0 -0
- package/src/match/voice/assets/number-4.wav +0 -0
- package/src/match/voice/assets/number-40.wav +0 -0
- package/src/match/voice/assets/number-5.wav +0 -0
- package/src/match/voice/assets/number-50.wav +0 -0
- package/src/match/voice/assets/number-6.wav +0 -0
- package/src/match/voice/assets/number-60.wav +0 -0
- package/src/match/voice/assets/number-7.wav +0 -0
- package/src/match/voice/assets/number-70.wav +0 -0
- package/src/match/voice/assets/number-8.wav +0 -0
- package/src/match/voice/assets/number-80.wav +0 -0
- package/src/match/voice/assets/number-9.wav +0 -0
- package/src/match/voice/assets/number-90.wav +0 -0
- package/src/match/voice/assets/number-lam.wav +0 -0
- package/src/match/voice/assets/number-mot.wav +0 -0
- package/src/match/voice/assets/score-deu-0.wav +0 -0
- package/src/match/voice/assets/score-deu-1.wav +0 -0
- package/src/match/voice/assets/score-deu-10.wav +0 -0
- package/src/match/voice/assets/score-deu-11.wav +0 -0
- package/src/match/voice/assets/score-deu-12.wav +0 -0
- package/src/match/voice/assets/score-deu-13.wav +0 -0
- package/src/match/voice/assets/score-deu-14.wav +0 -0
- package/src/match/voice/assets/score-deu-15.wav +0 -0
- package/src/match/voice/assets/score-deu-16.wav +0 -0
- package/src/match/voice/assets/score-deu-17.wav +0 -0
- package/src/match/voice/assets/score-deu-18.wav +0 -0
- package/src/match/voice/assets/score-deu-19.wav +0 -0
- package/src/match/voice/assets/score-deu-2.wav +0 -0
- package/src/match/voice/assets/score-deu-20.wav +0 -0
- package/src/match/voice/assets/score-deu-3.wav +0 -0
- package/src/match/voice/assets/score-deu-30.wav +0 -0
- package/src/match/voice/assets/score-deu-4.wav +0 -0
- package/src/match/voice/assets/score-deu-40.wav +0 -0
- package/src/match/voice/assets/score-deu-5.wav +0 -0
- package/src/match/voice/assets/score-deu-50.wav +0 -0
- package/src/match/voice/assets/score-deu-6.wav +0 -0
- package/src/match/voice/assets/score-deu-60.wav +0 -0
- package/src/match/voice/assets/score-deu-7.wav +0 -0
- package/src/match/voice/assets/score-deu-70.wav +0 -0
- package/src/match/voice/assets/score-deu-8.wav +0 -0
- package/src/match/voice/assets/score-deu-80.wav +0 -0
- package/src/match/voice/assets/score-deu-9.wav +0 -0
- package/src/match/voice/assets/score-deu-90.wav +0 -0
- package/src/match/voice/assets/score-deu-lam.wav +0 -0
- package/src/match/voice/assets/score-deu-mot.wav +0 -0
- package/src/match/voice/assets/server-1.wav +0 -0
- package/src/match/voice/assets/server-2.wav +0 -0
- package/src/match/voice/assets/service-change.wav +0 -0
- package/src/match/voice/assets/test-speaker.wav +0 -0
- package/src/match/voice/audio.ts +231 -0
- package/src/match/voice/bundled-assets.native.ts +47 -0
- package/src/match/voice/bundled-assets.ts +3 -0
- package/src/match/voice/gemini-engine.ts +167 -0
- package/src/match/voice/index.ts +7 -0
- package/src/match/voice/phrases.ts +185 -0
- package/src/match/voice/scheduler.ts +205 -0
- package/src/match/voice/service.ts +147 -0
- package/src/match/voice/types.ts +118 -0
- package/src/native-runtime.ts +241 -0
- package/src/plugin.ts +145 -0
- package/src/provider.tsx +176 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CourtSide,
|
|
3
|
+
MatchEvent,
|
|
4
|
+
MatchFormat,
|
|
5
|
+
MatchInputSource,
|
|
6
|
+
MatchScoringMode,
|
|
7
|
+
MatchSnapshot,
|
|
8
|
+
MatchTeamId,
|
|
9
|
+
} from "./contracts";
|
|
10
|
+
|
|
11
|
+
const TEAM_IDS = ["A", "B"] as const;
|
|
12
|
+
const COURT_SIDES = ["left", "right"] as const;
|
|
13
|
+
const INPUT_SOURCES = ["local", "remote", "voice", "ble", "system"] as const;
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
20
|
+
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertNonEmptyString(value: unknown, label: string): asserts value is string {
|
|
24
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
25
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertInteger(value: unknown, minimum: number, maximum: number, label: string) {
|
|
30
|
+
if (!Number.isInteger(value) || (value as number) < minimum || (value as number) > maximum) {
|
|
31
|
+
throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTeamId(value: unknown): value is MatchTeamId {
|
|
36
|
+
return value === "A" || value === "B";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isCourtSide(value: unknown): value is CourtSide {
|
|
40
|
+
return value === "left" || value === "right";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isFormat(value: unknown): value is MatchFormat {
|
|
44
|
+
return value === "singles" || value === "doubles";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isScoringMode(value: unknown): value is MatchScoringMode {
|
|
48
|
+
return value === "rally" || value === "sideOut";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isSource(value: unknown): value is MatchInputSource {
|
|
52
|
+
return INPUT_SOURCES.some((source) => source === value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function assertRecordKeys(
|
|
56
|
+
value: Record<string, unknown>,
|
|
57
|
+
allowed: readonly string[],
|
|
58
|
+
label: string,
|
|
59
|
+
) {
|
|
60
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) {
|
|
61
|
+
throw new TypeError(`${label} contains an unknown field`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function assertPendingSwitch(
|
|
66
|
+
value: unknown,
|
|
67
|
+
): asserts value is
|
|
68
|
+
| { reason: "gameEnd"; game: 1 | 2 }
|
|
69
|
+
| { reason: "gameThreeMidpoint"; game: 3 } {
|
|
70
|
+
assertRecord(value, "Pending court switch");
|
|
71
|
+
if (value.reason === "gameEnd") {
|
|
72
|
+
if (value.game !== 1 && value.game !== 2) {
|
|
73
|
+
throw new TypeError("Game-end switch must follow game one or two");
|
|
74
|
+
}
|
|
75
|
+
} else if (value.reason === "gameThreeMidpoint") {
|
|
76
|
+
if (value.game !== 3) throw new TypeError("Midpoint switch must be in game three");
|
|
77
|
+
} else {
|
|
78
|
+
throw new TypeError("Invalid pending court switch reason");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function assertRuntimeMatchSnapshot(
|
|
83
|
+
value: unknown,
|
|
84
|
+
): asserts value is MatchSnapshot {
|
|
85
|
+
assertRecord(value, "Match snapshot");
|
|
86
|
+
assertNonEmptyString(value.id, "Match id");
|
|
87
|
+
if (!isFormat(value.format)) throw new TypeError("Invalid match format");
|
|
88
|
+
if (!isScoringMode(value.scoringMode)) throw new TypeError("Invalid scoring mode");
|
|
89
|
+
assertInteger(value.targetScore, 1, 99, "Target score");
|
|
90
|
+
if (value.bestOf !== 3) throw new TypeError("Match must be best-of-three");
|
|
91
|
+
if (!isTeamId(value.initialServingTeam)) {
|
|
92
|
+
throw new TypeError("Invalid initial serving team");
|
|
93
|
+
}
|
|
94
|
+
assertInteger(value.sequence, 0, Number.MAX_SAFE_INTEGER, "Match sequence");
|
|
95
|
+
|
|
96
|
+
assertRecord(value.teams, "Teams");
|
|
97
|
+
assertRecordKeys(value.teams, TEAM_IDS, "Teams");
|
|
98
|
+
const expectedPlayers = value.format === "singles" ? 1 : 2;
|
|
99
|
+
const playerIds = new Set<string>();
|
|
100
|
+
const teamPlayerIds: Record<MatchTeamId, string[]> = { A: [], B: [] };
|
|
101
|
+
for (const teamId of TEAM_IDS) {
|
|
102
|
+
const team = value.teams[teamId];
|
|
103
|
+
assertRecord(team, `Team ${teamId}`);
|
|
104
|
+
if (team.id !== teamId || !Array.isArray(team.players) || team.players.length !== expectedPlayers) {
|
|
105
|
+
throw new TypeError(`Team ${teamId} roster is invalid`);
|
|
106
|
+
}
|
|
107
|
+
if (team.name !== undefined) assertNonEmptyString(team.name, `Team ${teamId} name`);
|
|
108
|
+
for (const rawPlayer of team.players) {
|
|
109
|
+
assertRecord(rawPlayer, "Player");
|
|
110
|
+
assertNonEmptyString(rawPlayer.id, "Player id");
|
|
111
|
+
assertNonEmptyString(rawPlayer.name, "Player name");
|
|
112
|
+
if (playerIds.has(rawPlayer.id)) throw new TypeError("Player ids must be unique");
|
|
113
|
+
playerIds.add(rawPlayer.id);
|
|
114
|
+
teamPlayerIds[teamId].push(rawPlayer.id);
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
team.initialServerId !== undefined &&
|
|
118
|
+
!teamPlayerIds[teamId].includes(team.initialServerId as string)
|
|
119
|
+
) {
|
|
120
|
+
throw new TypeError(`Team ${teamId} initial server is invalid`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
assertRecord(value.designatedServerByTeam, "Designated servers");
|
|
125
|
+
for (const teamId of TEAM_IDS) {
|
|
126
|
+
if (!teamPlayerIds[teamId].includes(value.designatedServerByTeam[teamId] as string)) {
|
|
127
|
+
throw new TypeError(`Team ${teamId} designated server is invalid`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
assertRecord(value.courtSides, "Display court mapping");
|
|
132
|
+
if (
|
|
133
|
+
!isTeamId(value.courtSides.left) ||
|
|
134
|
+
!isTeamId(value.courtSides.right) ||
|
|
135
|
+
value.courtSides.left === value.courtSides.right
|
|
136
|
+
) {
|
|
137
|
+
throw new TypeError("Display court mapping must contain both teams");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
assertRecord(value.playerCourts, "Player court mapping");
|
|
141
|
+
assertRecordKeys(value.playerCourts, [...playerIds], "Player court mapping");
|
|
142
|
+
if (Object.keys(value.playerCourts).length !== playerIds.size) {
|
|
143
|
+
throw new TypeError("Every player must have a court mapping");
|
|
144
|
+
}
|
|
145
|
+
for (const playerId of playerIds) {
|
|
146
|
+
if (!isCourtSide(value.playerCourts[playerId])) {
|
|
147
|
+
throw new TypeError("Player court mapping contains an invalid side");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (value.format === "doubles") {
|
|
151
|
+
for (const teamId of TEAM_IDS) {
|
|
152
|
+
const [first, second] = teamPlayerIds[teamId];
|
|
153
|
+
if (value.playerCourts[first] === value.playerCourts[second]) {
|
|
154
|
+
throw new TypeError(`Team ${teamId} players must occupy different courts`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!Array.isArray(value.games) || value.games.length < 1 || value.games.length > 3) {
|
|
160
|
+
throw new TypeError("Match games are invalid");
|
|
161
|
+
}
|
|
162
|
+
const games = value.games;
|
|
163
|
+
if (value.currentGame !== games.length) {
|
|
164
|
+
throw new TypeError("Current game must be the final game snapshot");
|
|
165
|
+
}
|
|
166
|
+
const winnerCounts = { A: 0, B: 0 };
|
|
167
|
+
games.forEach((rawGame, index) => {
|
|
168
|
+
assertRecord(rawGame, "Game snapshot");
|
|
169
|
+
if (rawGame.number !== index + 1 || !isTeamId(rawGame.startingServingTeam)) {
|
|
170
|
+
throw new TypeError("Game identity is invalid");
|
|
171
|
+
}
|
|
172
|
+
assertRecord(rawGame.score, "Game score");
|
|
173
|
+
for (const teamId of TEAM_IDS) {
|
|
174
|
+
assertInteger(rawGame.score[teamId], 0, value.targetScore as number, "Game score");
|
|
175
|
+
}
|
|
176
|
+
if (rawGame.winner !== null && !isTeamId(rawGame.winner)) {
|
|
177
|
+
throw new TypeError("Game winner is invalid");
|
|
178
|
+
}
|
|
179
|
+
if (
|
|
180
|
+
isTeamId(rawGame.winner) &&
|
|
181
|
+
rawGame.score[rawGame.winner] !== value.targetScore
|
|
182
|
+
) {
|
|
183
|
+
throw new TypeError("Winning game must reach target score");
|
|
184
|
+
}
|
|
185
|
+
if (rawGame.winner === null) {
|
|
186
|
+
if (
|
|
187
|
+
rawGame.score.A === value.targetScore ||
|
|
188
|
+
rawGame.score.B === value.targetScore
|
|
189
|
+
) {
|
|
190
|
+
throw new TypeError("A game at target score must have a winner");
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
const losingTeam = rawGame.winner === "A" ? "B" : "A";
|
|
194
|
+
if ((rawGame.score[losingTeam] as number) >= (value.targetScore as number)) {
|
|
195
|
+
throw new TypeError("The losing team must remain below target score");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isTeamId(rawGame.winner)) winnerCounts[rawGame.winner] += 1;
|
|
199
|
+
if (typeof rawGame.midGameSwitchCompleted !== "boolean") {
|
|
200
|
+
throw new TypeError("Game midpoint switch flag is invalid");
|
|
201
|
+
}
|
|
202
|
+
if (rawGame.midGameSwitchCompleted && rawGame.number !== 3) {
|
|
203
|
+
throw new TypeError("Only game three has a midpoint switch");
|
|
204
|
+
}
|
|
205
|
+
if (index < games.length - 1 && rawGame.winner === null) {
|
|
206
|
+
throw new TypeError("Previous games must have a winner");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
assertRecord(value.gamesWon, "Games won");
|
|
211
|
+
for (const teamId of TEAM_IDS) {
|
|
212
|
+
if (value.gamesWon[teamId] !== winnerCounts[teamId]) {
|
|
213
|
+
throw new TypeError("Games-won total does not match game history");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
assertRecord(value.serve, "Serve state");
|
|
218
|
+
if (
|
|
219
|
+
!isTeamId(value.serve.teamId) ||
|
|
220
|
+
!teamPlayerIds[value.serve.teamId].includes(value.serve.playerId as string) ||
|
|
221
|
+
(value.serve.serverNumber !== 1 && value.serve.serverNumber !== 2) ||
|
|
222
|
+
!isCourtSide(value.serve.court)
|
|
223
|
+
) {
|
|
224
|
+
throw new TypeError("Serve state is invalid");
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
(value.format === "singles" || value.scoringMode === "rally") &&
|
|
228
|
+
value.serve.serverNumber !== 1
|
|
229
|
+
) {
|
|
230
|
+
throw new TypeError("This match mode has one server");
|
|
231
|
+
}
|
|
232
|
+
const currentScore = (games[(value.currentGame as number) - 1] as Record<string, unknown>)
|
|
233
|
+
.score as Record<MatchTeamId, number>;
|
|
234
|
+
const parityCourt = currentScore[value.serve.teamId] % 2 === 0 ? "right" : "left";
|
|
235
|
+
if (
|
|
236
|
+
(value.format === "singles" ||
|
|
237
|
+
value.scoringMode === "rally" ||
|
|
238
|
+
value.serve.serverNumber === 1) &&
|
|
239
|
+
value.serve.court !== parityCourt
|
|
240
|
+
) {
|
|
241
|
+
throw new TypeError("Server court does not match score parity");
|
|
242
|
+
}
|
|
243
|
+
if (
|
|
244
|
+
value.format === "doubles" &&
|
|
245
|
+
value.playerCourts[value.serve.playerId as string] !== value.serve.court
|
|
246
|
+
) {
|
|
247
|
+
throw new TypeError("Server court does not match player position");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const currentGame = games[games.length - 1] as Record<string, unknown>;
|
|
251
|
+
if (value.pendingCourtSwitch !== null) {
|
|
252
|
+
assertPendingSwitch(value.pendingCourtSwitch);
|
|
253
|
+
const pending = value.pendingCourtSwitch;
|
|
254
|
+
if (pending.game !== value.currentGame) {
|
|
255
|
+
throw new TypeError("Pending switch must belong to the current game");
|
|
256
|
+
}
|
|
257
|
+
if (pending.reason === "gameEnd" && currentGame.winner === null) {
|
|
258
|
+
throw new TypeError("Game-end switch requires a game winner");
|
|
259
|
+
}
|
|
260
|
+
if (pending.reason === "gameThreeMidpoint") {
|
|
261
|
+
const score = currentGame.score as Record<MatchTeamId, number>;
|
|
262
|
+
if (
|
|
263
|
+
currentGame.winner !== null ||
|
|
264
|
+
currentGame.midGameSwitchCompleted !== false ||
|
|
265
|
+
Math.max(score.A, score.B) < Math.ceil((value.targetScore as number) / 2)
|
|
266
|
+
) {
|
|
267
|
+
throw new TypeError("Game-three midpoint switch state is invalid");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (value.status === "inProgress") {
|
|
273
|
+
if (winnerCounts.A >= 2 || winnerCounts.B >= 2) {
|
|
274
|
+
throw new TypeError("In-progress match cannot already have a match winner");
|
|
275
|
+
}
|
|
276
|
+
if (value.winner !== null || value.endReason !== null) {
|
|
277
|
+
throw new TypeError("In-progress match cannot have terminal metadata");
|
|
278
|
+
}
|
|
279
|
+
if (currentGame.winner !== null && value.pendingCourtSwitch === null) {
|
|
280
|
+
throw new TypeError("Completed game must await a court switch");
|
|
281
|
+
}
|
|
282
|
+
} else if (value.status === "completed") {
|
|
283
|
+
const matchWinners = TEAM_IDS.filter((teamId) => winnerCounts[teamId] === 2);
|
|
284
|
+
if (
|
|
285
|
+
matchWinners.length !== 1 ||
|
|
286
|
+
!isTeamId(value.winner) ||
|
|
287
|
+
matchWinners[0] !== value.winner ||
|
|
288
|
+
value.gamesWon[value.winner] !== 2 ||
|
|
289
|
+
currentGame.winner !== value.winner ||
|
|
290
|
+
value.endReason !== null ||
|
|
291
|
+
value.pendingCourtSwitch !== null
|
|
292
|
+
) {
|
|
293
|
+
throw new TypeError("Completed match state is invalid");
|
|
294
|
+
}
|
|
295
|
+
} else if (value.status === "ended") {
|
|
296
|
+
if (
|
|
297
|
+
winnerCounts.A >= 2 ||
|
|
298
|
+
winnerCounts.B >= 2 ||
|
|
299
|
+
value.winner !== null ||
|
|
300
|
+
typeof value.endReason !== "string" ||
|
|
301
|
+
value.endReason.trim().length === 0 ||
|
|
302
|
+
value.pendingCourtSwitch !== null
|
|
303
|
+
) {
|
|
304
|
+
throw new TypeError("Explicitly ended match state is invalid");
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
throw new TypeError("Match status is invalid");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function assertCorrection(value: unknown) {
|
|
312
|
+
assertRecord(value, "Match correction");
|
|
313
|
+
assertRecordKeys(value, ["score", "serve", "playerCourts"], "Match correction");
|
|
314
|
+
if (value.score === undefined && value.serve === undefined && value.playerCourts === undefined) {
|
|
315
|
+
throw new TypeError("Match correction is empty");
|
|
316
|
+
}
|
|
317
|
+
if (value.score !== undefined) {
|
|
318
|
+
assertRecord(value.score, "Corrected score");
|
|
319
|
+
assertRecordKeys(value.score, TEAM_IDS, "Corrected score");
|
|
320
|
+
for (const score of Object.values(value.score)) {
|
|
321
|
+
if (!Number.isInteger(score)) throw new TypeError("Corrected score must be an integer");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (value.serve !== undefined) {
|
|
325
|
+
assertRecord(value.serve, "Corrected serve");
|
|
326
|
+
if (
|
|
327
|
+
!isTeamId(value.serve.teamId) ||
|
|
328
|
+
typeof value.serve.playerId !== "string" ||
|
|
329
|
+
(value.serve.serverNumber !== 1 && value.serve.serverNumber !== 2) ||
|
|
330
|
+
!isCourtSide(value.serve.court)
|
|
331
|
+
) {
|
|
332
|
+
throw new TypeError("Corrected serve is malformed");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (value.playerCourts !== undefined) {
|
|
336
|
+
assertRecord(value.playerCourts, "Corrected player courts");
|
|
337
|
+
for (const court of Object.values(value.playerCourts)) {
|
|
338
|
+
if (!isCourtSide(court)) throw new TypeError("Corrected player court is malformed");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function assertRuntimeMatchEvent(value: unknown): asserts value is MatchEvent {
|
|
344
|
+
assertRecord(value, "Match event");
|
|
345
|
+
if (value.schemaVersion !== 1) throw new TypeError("Unsupported match event schema");
|
|
346
|
+
assertNonEmptyString(value.id, "Match event id");
|
|
347
|
+
assertInteger(value.sequence, 0, Number.MAX_SAFE_INTEGER, "Match event sequence");
|
|
348
|
+
if (!isSource(value.source)) throw new TypeError("Invalid match event source");
|
|
349
|
+
|
|
350
|
+
switch (value.type) {
|
|
351
|
+
case "matchCreated":
|
|
352
|
+
assertRuntimeMatchSnapshot(value.snapshot);
|
|
353
|
+
break;
|
|
354
|
+
case "rallyAwarded":
|
|
355
|
+
if (!isCourtSide(value.displaySide) || !isTeamId(value.winnerTeamId)) {
|
|
356
|
+
throw new TypeError("Rally event payload is invalid");
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
case "courtSwitchConfirmed":
|
|
360
|
+
assertPendingSwitch(value.pending);
|
|
361
|
+
break;
|
|
362
|
+
case "matchCorrected":
|
|
363
|
+
assertCorrection(value.correction);
|
|
364
|
+
break;
|
|
365
|
+
case "matchEnded":
|
|
366
|
+
assertNonEmptyString(value.reason, "Match end reason");
|
|
367
|
+
break;
|
|
368
|
+
case "matchEventUndone":
|
|
369
|
+
assertNonEmptyString(value.targetEventId, "Undo target event id");
|
|
370
|
+
assertRuntimeMatchSnapshot(value.restoredSnapshot);
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
throw new TypeError("Unknown match event type");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|