@tungstenstudio/darts-handicap-calculator 1.0.0-rc.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/README.md +328 -0
- package/dist/index.cjs +241 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/conversions.test.ts +41 -0
- package/src/conversions.ts +15 -0
- package/src/cricket.test.ts +818 -0
- package/src/cricket.ts +108 -0
- package/src/index.ts +19 -0
- package/src/types.ts +104 -0
- package/src/x01.test.ts +1145 -0
- package/src/x01.ts +118 -0
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @tungstenstudio/darts-handicap-calculator
|
|
2
|
+
|
|
3
|
+
Handicap calculator for x01 and cricket darts matches. Uses pure ratio methods — no lookup tables, flat offsets, or tiered brackets.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @tungstenstudio/darts-handicap-calculator
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @tungstenstudio/darts-handicap-calculator
|
|
11
|
+
# or
|
|
12
|
+
yarn add @tungstenstudio/darts-handicap-calculator
|
|
13
|
+
# or
|
|
14
|
+
bun add @tungstenstudio/darts-handicap-calculator
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## X01 Handicap
|
|
18
|
+
|
|
19
|
+
Scores are adjusted proportionally by the ratio of player averages (PPD), so both players need roughly the same number of darts to finish.
|
|
20
|
+
|
|
21
|
+
**Formula:** `adjustedScore = baseScore × (weakerPPD ÷ strongerPPD)`, rounded to the nearest integer.
|
|
22
|
+
|
|
23
|
+
### Quick Start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { calculateX01Handicap } from '@tungstenstudio/darts-handicap-calculator';
|
|
27
|
+
|
|
28
|
+
const result = calculateX01Handicap({
|
|
29
|
+
baseScore: 501,
|
|
30
|
+
direction: 'negative',
|
|
31
|
+
averageType: 'ppd',
|
|
32
|
+
playerA: { value: 22, name: 'Alice' },
|
|
33
|
+
playerB: { value: 16, name: 'Bob' },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
result.playerA.startScore; // 501 (stronger player keeps base)
|
|
37
|
+
result.playerB.startScore; // 364 (weaker player starts lower)
|
|
38
|
+
result.handicapPoints; // 137
|
|
39
|
+
result.playerA.estimatedDarts; // 23
|
|
40
|
+
result.playerB.estimatedDarts; // 23
|
|
41
|
+
result.playerA.name; // 'Alice'
|
|
42
|
+
result.capped; // false
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### With a Handicap Cap
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const result = calculateX01Handicap({
|
|
49
|
+
baseScore: 501,
|
|
50
|
+
direction: 'negative',
|
|
51
|
+
averageType: 'ppd',
|
|
52
|
+
playerA: { value: 20 },
|
|
53
|
+
playerB: { value: 10 },
|
|
54
|
+
maxHandicap: 150,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
result.playerA.startScore; // 501
|
|
58
|
+
result.playerB.startScore; // 351 (capped — would be 251 without cap)
|
|
59
|
+
result.handicapPoints; // 150
|
|
60
|
+
result.capped; // true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### With Score Rounding
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const result = calculateX01Handicap({
|
|
67
|
+
baseScore: 501,
|
|
68
|
+
direction: 'negative',
|
|
69
|
+
averageType: 'ppd',
|
|
70
|
+
playerA: { value: 17 },
|
|
71
|
+
playerB: { value: 12 },
|
|
72
|
+
roundTo: 25,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
result.playerB.startScore; // 350 (354 rounded to nearest 25)
|
|
76
|
+
result.rounded; // true
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### With Handicap Percentage
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const result = calculateX01Handicap({
|
|
83
|
+
baseScore: 501,
|
|
84
|
+
direction: 'negative',
|
|
85
|
+
averageType: 'ppd',
|
|
86
|
+
playerA: { value: 20 },
|
|
87
|
+
playerB: { value: 10 },
|
|
88
|
+
handicapPercentage: 75,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
result.playerB.startScore; // 313 (75% of 250-point gap applied)
|
|
92
|
+
result.handicapPoints; // 188
|
|
93
|
+
result.compressed; // true
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### API
|
|
97
|
+
|
|
98
|
+
#### `calculateX01Handicap(input: X01HandicapInput): X01HandicapResult`
|
|
99
|
+
|
|
100
|
+
Calculates handicapped start scores for two players.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
interface X01HandicapInput {
|
|
104
|
+
baseScore: number; // The base game score (e.g. 301, 501, 701)
|
|
105
|
+
direction: Direction; // 'negative' | 'positive'
|
|
106
|
+
averageType: AverageType; // 'ppd' | '3da'
|
|
107
|
+
playerA: PlayerAverage;
|
|
108
|
+
playerB: PlayerAverage;
|
|
109
|
+
maxHandicap?: number; // Optional cap on the handicap spread
|
|
110
|
+
roundTo?: number; // Round adjusted score to nearest N (e.g. 5, 10, 25)
|
|
111
|
+
handicapPercentage?: number; // Apply only N% of the handicap spread (1–100, default: 100)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface PlayerAverage {
|
|
115
|
+
value: number; // The player's average
|
|
116
|
+
name?: string; // Optional player name (passed through to result)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface X01HandicapResult {
|
|
124
|
+
baseScore: number;
|
|
125
|
+
direction: Direction;
|
|
126
|
+
handicapPoints: number; // Absolute difference between start scores
|
|
127
|
+
capped: boolean; // Whether maxHandicap was applied
|
|
128
|
+
rounded: boolean; // Whether roundTo changed the score
|
|
129
|
+
compressed: boolean; // Whether handicapPercentage reduced the spread
|
|
130
|
+
playerA: PlayerResult;
|
|
131
|
+
playerB: PlayerResult;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface PlayerResult {
|
|
135
|
+
startScore: number; // The handicapped start score
|
|
136
|
+
ppd: number; // Points per dart
|
|
137
|
+
threeDartAverage: number; // 3-dart average
|
|
138
|
+
estimatedDarts: number; // Estimated darts to finish (ceil(startScore / ppd))
|
|
139
|
+
name?: string; // Player name (if provided in input)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Direction
|
|
144
|
+
|
|
145
|
+
The `direction` parameter controls which player's score is adjusted:
|
|
146
|
+
|
|
147
|
+
| Direction | Stronger player | Weaker player |
|
|
148
|
+
|-----------|----------------|---------------|
|
|
149
|
+
| `'negative'` | Keeps the base score | Starts **lower** (reduced proportionally) |
|
|
150
|
+
| `'positive'` | Starts **higher** (raised proportionally) | Keeps the base score |
|
|
151
|
+
|
|
152
|
+
Both directions produce the same *relative* handicap. Choose based on your league's convention:
|
|
153
|
+
- **Negative** is the most common — it keeps the strong player at the standard game score.
|
|
154
|
+
- **Positive** is useful when you want all players to start at *at least* the base score.
|
|
155
|
+
|
|
156
|
+
## Cricket Handicap
|
|
157
|
+
|
|
158
|
+
The weaker player starts with "spot marks" — free marks toward closing numbers — so both players need roughly the same number of rounds to close all seven numbers.
|
|
159
|
+
|
|
160
|
+
**Formula:** `spotMarks = 21 × (1 − weakerAvg ÷ strongerAvg)`, rounded to the nearest integer. The total of 21 represents 3 marks × 7 cricket numbers.
|
|
161
|
+
|
|
162
|
+
### Quick Start
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { calculateCricketHandicap } from '@tungstenstudio/darts-handicap-calculator';
|
|
166
|
+
|
|
167
|
+
const result = calculateCricketHandicap({
|
|
168
|
+
averageType: 'mpr',
|
|
169
|
+
playerA: { value: 3.0, name: 'Alice' },
|
|
170
|
+
playerB: { value: 2.0, name: 'Bob' },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
result.spotMarks; // 7
|
|
174
|
+
result.playerA.startingMarks; // 0 (stronger player starts at 0)
|
|
175
|
+
result.playerB.startingMarks; // 7 (weaker player gets spot marks)
|
|
176
|
+
result.playerA.estimatedRounds; // 7
|
|
177
|
+
result.playerB.estimatedRounds; // 7
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### With a Handicap Cap
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const result = calculateCricketHandicap({
|
|
184
|
+
averageType: 'mpr',
|
|
185
|
+
playerA: { value: 4.0 },
|
|
186
|
+
playerB: { value: 1.0 },
|
|
187
|
+
maxHandicap: 10,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
result.spotMarks; // 10 (capped — would be 16 without cap)
|
|
191
|
+
result.capped; // true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### With Rounding Mode
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const result = calculateCricketHandicap({
|
|
198
|
+
averageType: 'mpr',
|
|
199
|
+
playerA: { value: 2.5 },
|
|
200
|
+
playerB: { value: 2.0 },
|
|
201
|
+
rounding: 'ceil', // 'round' (default) | 'ceil' | 'floor'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
result.spotMarks; // 5 (4.2 ceiled — would be 4 with default rounding)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### With Handicap Percentage
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const result = calculateCricketHandicap({
|
|
211
|
+
averageType: 'mpr',
|
|
212
|
+
playerA: { value: 3.0 },
|
|
213
|
+
playerB: { value: 2.0 },
|
|
214
|
+
handicapPercentage: 50,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
result.spotMarks; // 4 (50% of 7 spot marks)
|
|
218
|
+
result.compressed; // true
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### API
|
|
222
|
+
|
|
223
|
+
#### `calculateCricketHandicap(input: CricketHandicapInput): CricketHandicapResult`
|
|
224
|
+
|
|
225
|
+
Calculates spot marks for two cricket players.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
interface CricketHandicapInput {
|
|
229
|
+
averageType: CricketAverageType; // 'mpr' | 'ppd'
|
|
230
|
+
playerA: PlayerAverage;
|
|
231
|
+
playerB: PlayerAverage;
|
|
232
|
+
maxHandicap?: number; // Optional cap on spot marks
|
|
233
|
+
rounding?: CricketRounding; // 'round' (default) | 'ceil' | 'floor'
|
|
234
|
+
handicapPercentage?: number; // Apply only N% of the spot marks (1–100, default: 100)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
interface CricketHandicapResult {
|
|
242
|
+
spotMarks: number; // Marks spotted to the weaker player
|
|
243
|
+
capped: boolean; // Whether maxHandicap was applied
|
|
244
|
+
rounding: CricketRounding; // The rounding mode used
|
|
245
|
+
compressed: boolean; // Whether handicapPercentage reduced the spread
|
|
246
|
+
playerA: CricketPlayerResult;
|
|
247
|
+
playerB: CricketPlayerResult;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface CricketPlayerResult {
|
|
251
|
+
startingMarks: number; // 0 for stronger player, spotMarks for weaker
|
|
252
|
+
marksNeeded: number; // 21 − startingMarks
|
|
253
|
+
average: number; // The player's average
|
|
254
|
+
averageType: CricketAverageType; // 'mpr' | 'ppd'
|
|
255
|
+
estimatedRounds: number; // ceil(marksNeeded / average)
|
|
256
|
+
name?: string; // Player name (if provided in input)
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Utility Functions
|
|
261
|
+
|
|
262
|
+
#### `ppdTo3DA(ppd: number): number`
|
|
263
|
+
|
|
264
|
+
Converts points-per-dart to 3-dart average.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
ppdTo3DA(20); // 60
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### `threeDAToPPD(tda: number): number`
|
|
271
|
+
|
|
272
|
+
Converts 3-dart average to points-per-dart.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
threeDAToPPD(60); // 20
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Error Handling
|
|
279
|
+
|
|
280
|
+
The calculator throws `HandicapError` (extends `Error`) with a `code` property for invalid inputs:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { calculateX01Handicap, HandicapError } from '@tungstenstudio/darts-handicap-calculator';
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
calculateX01Handicap(input);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err instanceof HandicapError) {
|
|
289
|
+
console.error(err.code); // e.g. 'INVALID_PLAYER_A_AVERAGE'
|
|
290
|
+
console.error(err.message); // Human-readable description
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| Error Code | Cause |
|
|
296
|
+
|------------|-------|
|
|
297
|
+
| `INVALID_BASE_SCORE` | `baseScore` is not a finite positive number |
|
|
298
|
+
| `INVALID_PLAYER_A_AVERAGE` | Player A's average is not a finite positive number |
|
|
299
|
+
| `INVALID_PLAYER_B_AVERAGE` | Player B's average is not a finite positive number |
|
|
300
|
+
| `INVALID_MAX_HANDICAP` | `maxHandicap` is provided but not a finite positive number |
|
|
301
|
+
| `INVALID_ROUND_TO` | `roundTo` is provided but not a finite positive integer |
|
|
302
|
+
| `INVALID_HANDICAP_PERCENTAGE` | `handicapPercentage` is provided but not a finite number between 1 and 100 |
|
|
303
|
+
| `INVALID_ROUNDING` | `rounding` (cricket) is provided but not `'round'`, `'ceil'`, or `'floor'` |
|
|
304
|
+
|
|
305
|
+
## Types
|
|
306
|
+
|
|
307
|
+
All TypeScript types are exported for convenience:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import type {
|
|
311
|
+
Direction,
|
|
312
|
+
AverageType,
|
|
313
|
+
PlayerAverage,
|
|
314
|
+
PlayerResult,
|
|
315
|
+
X01HandicapInput,
|
|
316
|
+
X01HandicapResult,
|
|
317
|
+
CricketAverageType,
|
|
318
|
+
CricketRounding,
|
|
319
|
+
CricketHandicapInput,
|
|
320
|
+
CricketPlayerResult,
|
|
321
|
+
CricketHandicapResult,
|
|
322
|
+
HandicapErrorCode,
|
|
323
|
+
} from '@tungstenstudio/darts-handicap-calculator';
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HandicapError: () => HandicapError,
|
|
24
|
+
calculateCricketHandicap: () => calculateCricketHandicap,
|
|
25
|
+
calculateX01Handicap: () => calculateX01Handicap,
|
|
26
|
+
ppdTo3DA: () => ppdTo3DA,
|
|
27
|
+
threeDAToPPD: () => threeDAToPPD
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/types.ts
|
|
32
|
+
var HandicapError = class extends Error {
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "HandicapError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/conversions.ts
|
|
41
|
+
function ppdTo3DA(ppd) {
|
|
42
|
+
return ppd * 3;
|
|
43
|
+
}
|
|
44
|
+
function threeDAToPPD(tda) {
|
|
45
|
+
return tda / 3;
|
|
46
|
+
}
|
|
47
|
+
function toPPD(value, type) {
|
|
48
|
+
if (type === "3da") return threeDAToPPD(value);
|
|
49
|
+
if (type === "ppd") return value;
|
|
50
|
+
throw new Error(`Unknown average type: ${type}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/x01.ts
|
|
54
|
+
function calculateX01Handicap(input) {
|
|
55
|
+
const { baseScore, direction, averageType, playerA, playerB, maxHandicap, roundTo, handicapPercentage } = input;
|
|
56
|
+
if (!Number.isFinite(baseScore) || baseScore <= 0) {
|
|
57
|
+
throw new HandicapError("baseScore must be a finite positive number", "INVALID_BASE_SCORE");
|
|
58
|
+
}
|
|
59
|
+
const ppdA = toPPD(playerA.value, averageType);
|
|
60
|
+
if (!Number.isFinite(ppdA) || ppdA <= 0) {
|
|
61
|
+
throw new HandicapError("Player A average must be a finite positive number", "INVALID_PLAYER_A_AVERAGE");
|
|
62
|
+
}
|
|
63
|
+
const ppdB = toPPD(playerB.value, averageType);
|
|
64
|
+
if (!Number.isFinite(ppdB) || ppdB <= 0) {
|
|
65
|
+
throw new HandicapError("Player B average must be a finite positive number", "INVALID_PLAYER_B_AVERAGE");
|
|
66
|
+
}
|
|
67
|
+
if (maxHandicap !== void 0) {
|
|
68
|
+
if (!Number.isFinite(maxHandicap) || maxHandicap <= 0) {
|
|
69
|
+
throw new HandicapError("maxHandicap must be a finite positive number", "INVALID_MAX_HANDICAP");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (roundTo !== void 0) {
|
|
73
|
+
if (!Number.isFinite(roundTo) || roundTo <= 0 || !Number.isInteger(roundTo)) {
|
|
74
|
+
throw new HandicapError("roundTo must be a finite positive integer", "INVALID_ROUND_TO");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (handicapPercentage !== void 0) {
|
|
78
|
+
if (!Number.isFinite(handicapPercentage) || handicapPercentage < 1 || handicapPercentage > 100) {
|
|
79
|
+
throw new HandicapError("handicapPercentage must be a finite number between 1 and 100", "INVALID_HANDICAP_PERCENTAGE");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const strongerPPD = Math.max(ppdA, ppdB);
|
|
83
|
+
const weakerPPD = Math.min(ppdA, ppdB);
|
|
84
|
+
let strongerStart;
|
|
85
|
+
let weakerStart;
|
|
86
|
+
if (direction === "negative") {
|
|
87
|
+
strongerStart = baseScore;
|
|
88
|
+
weakerStart = Math.round(baseScore * (weakerPPD / strongerPPD));
|
|
89
|
+
} else {
|
|
90
|
+
weakerStart = baseScore;
|
|
91
|
+
strongerStart = Math.round(baseScore * (strongerPPD / weakerPPD));
|
|
92
|
+
}
|
|
93
|
+
let compressed = false;
|
|
94
|
+
if (handicapPercentage !== void 0 && handicapPercentage < 100) {
|
|
95
|
+
const rawDiff = Math.abs(strongerStart - weakerStart);
|
|
96
|
+
const adjustedDiff = Math.round(rawDiff * (handicapPercentage / 100));
|
|
97
|
+
if (adjustedDiff !== rawDiff) {
|
|
98
|
+
compressed = true;
|
|
99
|
+
if (direction === "negative") {
|
|
100
|
+
weakerStart = strongerStart - adjustedDiff;
|
|
101
|
+
} else {
|
|
102
|
+
strongerStart = weakerStart + adjustedDiff;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let capped = false;
|
|
107
|
+
const rawHandicap = Math.abs(strongerStart - weakerStart);
|
|
108
|
+
if (maxHandicap !== void 0 && rawHandicap > maxHandicap) {
|
|
109
|
+
capped = true;
|
|
110
|
+
if (direction === "negative") {
|
|
111
|
+
weakerStart = strongerStart - maxHandicap;
|
|
112
|
+
} else {
|
|
113
|
+
strongerStart = weakerStart + maxHandicap;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let rounded = false;
|
|
117
|
+
if (roundTo !== void 0 && roundTo > 1) {
|
|
118
|
+
if (direction === "negative") {
|
|
119
|
+
const before = weakerStart;
|
|
120
|
+
weakerStart = Math.max(roundTo, Math.round(weakerStart / roundTo) * roundTo);
|
|
121
|
+
rounded = weakerStart !== before;
|
|
122
|
+
} else {
|
|
123
|
+
const before = strongerStart;
|
|
124
|
+
strongerStart = Math.max(roundTo, Math.round(strongerStart / roundTo) * roundTo);
|
|
125
|
+
rounded = strongerStart !== before;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const aIsStronger = ppdA >= ppdB;
|
|
129
|
+
function buildResult(ppd, startScore, avg) {
|
|
130
|
+
const result = {
|
|
131
|
+
startScore,
|
|
132
|
+
ppd,
|
|
133
|
+
threeDartAverage: ppdTo3DA(ppd),
|
|
134
|
+
estimatedDarts: Math.ceil(startScore / ppd)
|
|
135
|
+
};
|
|
136
|
+
if (avg.name !== void 0) {
|
|
137
|
+
result.name = avg.name;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
baseScore,
|
|
143
|
+
direction,
|
|
144
|
+
handicapPoints: Math.abs(strongerStart - weakerStart),
|
|
145
|
+
capped,
|
|
146
|
+
rounded,
|
|
147
|
+
compressed,
|
|
148
|
+
playerA: buildResult(ppdA, aIsStronger ? strongerStart : weakerStart, playerA),
|
|
149
|
+
playerB: buildResult(ppdB, aIsStronger ? weakerStart : strongerStart, playerB)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/cricket.ts
|
|
154
|
+
var TOTAL_MARKS = 21;
|
|
155
|
+
var VALID_ROUNDING = ["round", "ceil", "floor"];
|
|
156
|
+
function roundFn(mode) {
|
|
157
|
+
if (mode === "ceil") return Math.ceil;
|
|
158
|
+
if (mode === "floor") return Math.floor;
|
|
159
|
+
return Math.round;
|
|
160
|
+
}
|
|
161
|
+
function calculateCricketHandicap(input) {
|
|
162
|
+
const { averageType, playerA, playerB, maxHandicap, handicapPercentage } = input;
|
|
163
|
+
const rounding = input.rounding ?? "round";
|
|
164
|
+
const avgA = playerA.value;
|
|
165
|
+
if (!Number.isFinite(avgA) || avgA <= 0) {
|
|
166
|
+
throw new HandicapError("Player A average must be a finite positive number", "INVALID_PLAYER_A_AVERAGE");
|
|
167
|
+
}
|
|
168
|
+
const avgB = playerB.value;
|
|
169
|
+
if (!Number.isFinite(avgB) || avgB <= 0) {
|
|
170
|
+
throw new HandicapError("Player B average must be a finite positive number", "INVALID_PLAYER_B_AVERAGE");
|
|
171
|
+
}
|
|
172
|
+
if (maxHandicap !== void 0) {
|
|
173
|
+
if (!Number.isFinite(maxHandicap) || maxHandicap <= 0) {
|
|
174
|
+
throw new HandicapError("maxHandicap must be a finite positive number", "INVALID_MAX_HANDICAP");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (input.rounding !== void 0) {
|
|
178
|
+
if (!VALID_ROUNDING.includes(input.rounding)) {
|
|
179
|
+
throw new HandicapError(
|
|
180
|
+
"rounding must be 'round', 'ceil', or 'floor'",
|
|
181
|
+
"INVALID_ROUNDING"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (handicapPercentage !== void 0) {
|
|
186
|
+
if (!Number.isFinite(handicapPercentage) || handicapPercentage < 1 || handicapPercentage > 100) {
|
|
187
|
+
throw new HandicapError("handicapPercentage must be a finite number between 1 and 100", "INVALID_HANDICAP_PERCENTAGE");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const strongerAvg = Math.max(avgA, avgB);
|
|
191
|
+
const weakerAvg = Math.min(avgA, avgB);
|
|
192
|
+
const rawSpotMarks = TOTAL_MARKS * (1 - weakerAvg / strongerAvg);
|
|
193
|
+
const round = roundFn(rounding);
|
|
194
|
+
let spotMarks = round(rawSpotMarks);
|
|
195
|
+
let compressed = false;
|
|
196
|
+
if (handicapPercentage !== void 0 && handicapPercentage < 100) {
|
|
197
|
+
const adjustedMarks = round(spotMarks * (handicapPercentage / 100));
|
|
198
|
+
if (adjustedMarks !== spotMarks) {
|
|
199
|
+
compressed = true;
|
|
200
|
+
spotMarks = adjustedMarks;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let capped = false;
|
|
204
|
+
if (maxHandicap !== void 0 && spotMarks > maxHandicap) {
|
|
205
|
+
capped = true;
|
|
206
|
+
spotMarks = maxHandicap;
|
|
207
|
+
}
|
|
208
|
+
const aIsStronger = avgA >= avgB;
|
|
209
|
+
function buildResult(avg, isStronger) {
|
|
210
|
+
const startingMarks = isStronger ? 0 : spotMarks;
|
|
211
|
+
const marksNeeded = TOTAL_MARKS - startingMarks;
|
|
212
|
+
const result = {
|
|
213
|
+
startingMarks,
|
|
214
|
+
marksNeeded,
|
|
215
|
+
average: avg.value,
|
|
216
|
+
averageType,
|
|
217
|
+
estimatedRounds: Math.ceil(marksNeeded / avg.value)
|
|
218
|
+
};
|
|
219
|
+
if (avg.name !== void 0) {
|
|
220
|
+
result.name = avg.name;
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
spotMarks,
|
|
226
|
+
capped,
|
|
227
|
+
rounding,
|
|
228
|
+
compressed,
|
|
229
|
+
playerA: buildResult(playerA, aIsStronger),
|
|
230
|
+
playerB: buildResult(playerB, !aIsStronger)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
234
|
+
0 && (module.exports = {
|
|
235
|
+
HandicapError,
|
|
236
|
+
calculateCricketHandicap,
|
|
237
|
+
calculateX01Handicap,
|
|
238
|
+
ppdTo3DA,
|
|
239
|
+
threeDAToPPD
|
|
240
|
+
});
|
|
241
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/conversions.ts","../src/x01.ts","../src/cricket.ts"],"sourcesContent":["export type {\n Direction,\n AverageType,\n PlayerAverage,\n PlayerResult,\n X01HandicapInput,\n X01HandicapResult,\n HandicapErrorCode,\n CricketAverageType,\n CricketRounding,\n CricketHandicapInput,\n CricketPlayerResult,\n CricketHandicapResult,\n} from './types.js';\n\nexport { HandicapError } from './types.js';\nexport { ppdTo3DA, threeDAToPPD } from './conversions.js';\nexport { calculateX01Handicap } from './x01.js';\nexport { calculateCricketHandicap } from './cricket.js';\n","/**\n * Controls which player's start score is adjusted.\n *\n * - `'negative'` — The stronger player keeps the base score; the weaker\n * player's start score is reduced proportionally.\n * - `'positive'` — The weaker player keeps the base score; the stronger\n * player's start score is raised proportionally.\n */\nexport type Direction = 'positive' | 'negative';\n\nexport type AverageType = 'ppd' | '3da';\n\nexport interface PlayerAverage {\n value: number;\n name?: string;\n}\n\nexport interface PlayerResult {\n startScore: number;\n ppd: number;\n threeDartAverage: number;\n estimatedDarts: number;\n name?: string;\n}\n\nexport interface X01HandicapInput {\n baseScore: number;\n direction: Direction;\n averageType: AverageType;\n playerA: PlayerAverage;\n playerB: PlayerAverage;\n maxHandicap?: number;\n roundTo?: number;\n handicapPercentage?: number;\n}\n\nexport interface X01HandicapResult {\n baseScore: number;\n direction: Direction;\n /** Absolute difference between the two start scores. */\n handicapPoints: number;\n /** Whether the handicap was capped by maxHandicap. */\n capped: boolean;\n /** Whether the start score was rounded by roundTo. */\n rounded: boolean;\n /** Whether handicapPercentage compressed the spread. */\n compressed: boolean;\n playerA: PlayerResult;\n playerB: PlayerResult;\n}\n\nexport type HandicapErrorCode =\n | 'INVALID_BASE_SCORE'\n | 'INVALID_PLAYER_A_AVERAGE'\n | 'INVALID_PLAYER_B_AVERAGE'\n | 'INVALID_MAX_HANDICAP'\n | 'INVALID_ROUND_TO'\n | 'INVALID_ROUNDING'\n | 'INVALID_HANDICAP_PERCENTAGE';\n\nexport class HandicapError extends Error {\n constructor(\n message: string,\n public readonly code: HandicapErrorCode,\n ) {\n super(message);\n this.name = 'HandicapError';\n }\n}\n\nexport type CricketAverageType = 'mpr' | 'ppd';\n\nexport type CricketRounding = 'round' | 'ceil' | 'floor';\n\nexport interface CricketHandicapInput {\n averageType: CricketAverageType;\n playerA: PlayerAverage;\n playerB: PlayerAverage;\n maxHandicap?: number;\n rounding?: CricketRounding;\n handicapPercentage?: number;\n}\n\nexport interface CricketPlayerResult {\n startingMarks: number;\n marksNeeded: number;\n average: number;\n averageType: CricketAverageType;\n estimatedRounds: number;\n name?: string;\n}\n\nexport interface CricketHandicapResult {\n /** Number of marks spotted to the weaker player. */\n spotMarks: number;\n /** Whether the handicap was capped by maxHandicap. */\n capped: boolean;\n /** The rounding mode used ('round' | 'ceil' | 'floor'). */\n rounding: CricketRounding;\n /** Whether handicapPercentage compressed the spread. */\n compressed: boolean;\n playerA: CricketPlayerResult;\n playerB: CricketPlayerResult;\n}\n","import type { AverageType } from './types.js';\n\nexport function ppdTo3DA(ppd: number): number {\n return ppd * 3;\n}\n\nexport function threeDAToPPD(tda: number): number {\n return tda / 3;\n}\n\nexport function toPPD(value: number, type: AverageType): number {\n if (type === '3da') return threeDAToPPD(value);\n if (type === 'ppd') return value;\n throw new Error(`Unknown average type: ${type as string}`);\n}\n","import type { X01HandicapInput, X01HandicapResult, PlayerResult, PlayerAverage } from './types.js';\nimport { HandicapError } from './types.js';\nimport { toPPD, ppdTo3DA } from './conversions.js';\n\nexport function calculateX01Handicap(input: X01HandicapInput): X01HandicapResult {\n const { baseScore, direction, averageType, playerA, playerB, maxHandicap, roundTo, handicapPercentage } = input;\n\n if (!Number.isFinite(baseScore) || baseScore <= 0) {\n throw new HandicapError('baseScore must be a finite positive number', 'INVALID_BASE_SCORE');\n }\n\n const ppdA = toPPD(playerA.value, averageType);\n if (!Number.isFinite(ppdA) || ppdA <= 0) {\n throw new HandicapError('Player A average must be a finite positive number', 'INVALID_PLAYER_A_AVERAGE');\n }\n\n const ppdB = toPPD(playerB.value, averageType);\n if (!Number.isFinite(ppdB) || ppdB <= 0) {\n throw new HandicapError('Player B average must be a finite positive number', 'INVALID_PLAYER_B_AVERAGE');\n }\n\n if (maxHandicap !== undefined) {\n if (!Number.isFinite(maxHandicap) || maxHandicap <= 0) {\n throw new HandicapError('maxHandicap must be a finite positive number', 'INVALID_MAX_HANDICAP');\n }\n }\n\n if (roundTo !== undefined) {\n if (!Number.isFinite(roundTo) || roundTo <= 0 || !Number.isInteger(roundTo)) {\n throw new HandicapError('roundTo must be a finite positive integer', 'INVALID_ROUND_TO');\n }\n }\n\n if (handicapPercentage !== undefined) {\n if (!Number.isFinite(handicapPercentage) || handicapPercentage < 1 || handicapPercentage > 100) {\n throw new HandicapError('handicapPercentage must be a finite number between 1 and 100', 'INVALID_HANDICAP_PERCENTAGE');\n }\n }\n\n const strongerPPD = Math.max(ppdA, ppdB);\n const weakerPPD = Math.min(ppdA, ppdB);\n\n let strongerStart: number;\n let weakerStart: number;\n\n if (direction === 'negative') {\n strongerStart = baseScore;\n weakerStart = Math.round(baseScore * (weakerPPD / strongerPPD));\n } else {\n weakerStart = baseScore;\n strongerStart = Math.round(baseScore * (strongerPPD / weakerPPD));\n }\n\n let compressed = false;\n if (handicapPercentage !== undefined && handicapPercentage < 100) {\n const rawDiff = Math.abs(strongerStart - weakerStart);\n const adjustedDiff = Math.round(rawDiff * (handicapPercentage / 100));\n if (adjustedDiff !== rawDiff) {\n compressed = true;\n if (direction === 'negative') {\n weakerStart = strongerStart - adjustedDiff;\n } else {\n strongerStart = weakerStart + adjustedDiff;\n }\n }\n }\n\n let capped = false;\n const rawHandicap = Math.abs(strongerStart - weakerStart);\n\n if (maxHandicap !== undefined && rawHandicap > maxHandicap) {\n capped = true;\n if (direction === 'negative') {\n weakerStart = strongerStart - maxHandicap;\n } else {\n strongerStart = weakerStart + maxHandicap;\n }\n }\n\n let rounded = false;\n if (roundTo !== undefined && roundTo > 1) {\n if (direction === 'negative') {\n const before = weakerStart;\n weakerStart = Math.max(roundTo, Math.round(weakerStart / roundTo) * roundTo);\n rounded = weakerStart !== before;\n } else {\n const before = strongerStart;\n strongerStart = Math.max(roundTo, Math.round(strongerStart / roundTo) * roundTo);\n rounded = strongerStart !== before;\n }\n }\n\n const aIsStronger = ppdA >= ppdB;\n\n function buildResult(ppd: number, startScore: number, avg: PlayerAverage): PlayerResult {\n const result: PlayerResult = {\n startScore,\n ppd,\n threeDartAverage: ppdTo3DA(ppd),\n estimatedDarts: Math.ceil(startScore / ppd),\n };\n if (avg.name !== undefined) {\n result.name = avg.name;\n }\n return result;\n }\n\n return {\n baseScore,\n direction,\n handicapPoints: Math.abs(strongerStart - weakerStart),\n capped,\n rounded,\n compressed,\n playerA: buildResult(ppdA, aIsStronger ? strongerStart : weakerStart, playerA),\n playerB: buildResult(ppdB, aIsStronger ? weakerStart : strongerStart, playerB),\n };\n}\n","import type {\n CricketHandicapInput,\n CricketHandicapResult,\n CricketPlayerResult,\n CricketAverageType,\n CricketRounding,\n PlayerAverage,\n} from './types.js';\nimport { HandicapError } from './types.js';\n\nconst TOTAL_MARKS = 21;\nconst VALID_ROUNDING: CricketRounding[] = ['round', 'ceil', 'floor'];\n\nfunction roundFn(mode: CricketRounding): (n: number) => number {\n if (mode === 'ceil') return Math.ceil;\n if (mode === 'floor') return Math.floor;\n return Math.round;\n}\n\nexport function calculateCricketHandicap(input: CricketHandicapInput): CricketHandicapResult {\n const { averageType, playerA, playerB, maxHandicap, handicapPercentage } = input;\n const rounding: CricketRounding = input.rounding ?? 'round';\n\n const avgA = playerA.value;\n if (!Number.isFinite(avgA) || avgA <= 0) {\n throw new HandicapError('Player A average must be a finite positive number', 'INVALID_PLAYER_A_AVERAGE');\n }\n\n const avgB = playerB.value;\n if (!Number.isFinite(avgB) || avgB <= 0) {\n throw new HandicapError('Player B average must be a finite positive number', 'INVALID_PLAYER_B_AVERAGE');\n }\n\n if (maxHandicap !== undefined) {\n if (!Number.isFinite(maxHandicap) || maxHandicap <= 0) {\n throw new HandicapError('maxHandicap must be a finite positive number', 'INVALID_MAX_HANDICAP');\n }\n }\n\n if (input.rounding !== undefined) {\n if (!VALID_ROUNDING.includes(input.rounding)) {\n throw new HandicapError(\n \"rounding must be 'round', 'ceil', or 'floor'\",\n 'INVALID_ROUNDING',\n );\n }\n }\n\n if (handicapPercentage !== undefined) {\n if (!Number.isFinite(handicapPercentage) || handicapPercentage < 1 || handicapPercentage > 100) {\n throw new HandicapError('handicapPercentage must be a finite number between 1 and 100', 'INVALID_HANDICAP_PERCENTAGE');\n }\n }\n\n const strongerAvg = Math.max(avgA, avgB);\n const weakerAvg = Math.min(avgA, avgB);\n\n // Raw spot marks: 21 × (1 - weaker/stronger)\n const rawSpotMarks = TOTAL_MARKS * (1 - weakerAvg / strongerAvg);\n\n // Apply rounding mode\n const round = roundFn(rounding);\n let spotMarks = round(rawSpotMarks);\n\n // Apply handicapPercentage compression\n let compressed = false;\n if (handicapPercentage !== undefined && handicapPercentage < 100) {\n const adjustedMarks = round(spotMarks * (handicapPercentage / 100));\n if (adjustedMarks !== spotMarks) {\n compressed = true;\n spotMarks = adjustedMarks;\n }\n }\n\n // Apply maxHandicap cap\n let capped = false;\n if (maxHandicap !== undefined && spotMarks > maxHandicap) {\n capped = true;\n spotMarks = maxHandicap;\n }\n\n const aIsStronger = avgA >= avgB;\n\n function buildResult(avg: PlayerAverage, isStronger: boolean): CricketPlayerResult {\n const startingMarks = isStronger ? 0 : spotMarks;\n const marksNeeded = TOTAL_MARKS - startingMarks;\n const result: CricketPlayerResult = {\n startingMarks,\n marksNeeded,\n average: avg.value,\n averageType,\n estimatedRounds: Math.ceil(marksNeeded / avg.value),\n };\n if (avg.name !== undefined) {\n result.name = avg.name;\n }\n return result;\n }\n\n return {\n spotMarks,\n capped,\n rounding,\n compressed,\n playerA: buildResult(playerA, aIsStronger),\n playerB: buildResult(playerB, !aIsStronger),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controls which player's start score is adjusted.
|
|
3
|
+
*
|
|
4
|
+
* - `'negative'` — The stronger player keeps the base score; the weaker
|
|
5
|
+
* player's start score is reduced proportionally.
|
|
6
|
+
* - `'positive'` — The weaker player keeps the base score; the stronger
|
|
7
|
+
* player's start score is raised proportionally.
|
|
8
|
+
*/
|
|
9
|
+
type Direction = 'positive' | 'negative';
|
|
10
|
+
type AverageType = 'ppd' | '3da';
|
|
11
|
+
interface PlayerAverage {
|
|
12
|
+
value: number;
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
interface PlayerResult {
|
|
16
|
+
startScore: number;
|
|
17
|
+
ppd: number;
|
|
18
|
+
threeDartAverage: number;
|
|
19
|
+
estimatedDarts: number;
|
|
20
|
+
name?: string;
|
|
21
|
+
}
|
|
22
|
+
interface X01HandicapInput {
|
|
23
|
+
baseScore: number;
|
|
24
|
+
direction: Direction;
|
|
25
|
+
averageType: AverageType;
|
|
26
|
+
playerA: PlayerAverage;
|
|
27
|
+
playerB: PlayerAverage;
|
|
28
|
+
maxHandicap?: number;
|
|
29
|
+
roundTo?: number;
|
|
30
|
+
handicapPercentage?: number;
|
|
31
|
+
}
|
|
32
|
+
interface X01HandicapResult {
|
|
33
|
+
baseScore: number;
|
|
34
|
+
direction: Direction;
|
|
35
|
+
/** Absolute difference between the two start scores. */
|
|
36
|
+
handicapPoints: number;
|
|
37
|
+
/** Whether the handicap was capped by maxHandicap. */
|
|
38
|
+
capped: boolean;
|
|
39
|
+
/** Whether the start score was rounded by roundTo. */
|
|
40
|
+
rounded: boolean;
|
|
41
|
+
/** Whether handicapPercentage compressed the spread. */
|
|
42
|
+
compressed: boolean;
|
|
43
|
+
playerA: PlayerResult;
|
|
44
|
+
playerB: PlayerResult;
|
|
45
|
+
}
|
|
46
|
+
type HandicapErrorCode = 'INVALID_BASE_SCORE' | 'INVALID_PLAYER_A_AVERAGE' | 'INVALID_PLAYER_B_AVERAGE' | 'INVALID_MAX_HANDICAP' | 'INVALID_ROUND_TO' | 'INVALID_ROUNDING' | 'INVALID_HANDICAP_PERCENTAGE';
|
|
47
|
+
declare class HandicapError extends Error {
|
|
48
|
+
readonly code: HandicapErrorCode;
|
|
49
|
+
constructor(message: string, code: HandicapErrorCode);
|
|
50
|
+
}
|
|
51
|
+
type CricketAverageType = 'mpr' | 'ppd';
|
|
52
|
+
type CricketRounding = 'round' | 'ceil' | 'floor';
|
|
53
|
+
interface CricketHandicapInput {
|
|
54
|
+
averageType: CricketAverageType;
|
|
55
|
+
playerA: PlayerAverage;
|
|
56
|
+
playerB: PlayerAverage;
|
|
57
|
+
maxHandicap?: number;
|
|
58
|
+
rounding?: CricketRounding;
|
|
59
|
+
handicapPercentage?: number;
|
|
60
|
+
}
|
|
61
|
+
interface CricketPlayerResult {
|
|
62
|
+
startingMarks: number;
|
|
63
|
+
marksNeeded: number;
|
|
64
|
+
average: number;
|
|
65
|
+
averageType: CricketAverageType;
|
|
66
|
+
estimatedRounds: number;
|
|
67
|
+
name?: string;
|
|
68
|
+
}
|
|
69
|
+
interface CricketHandicapResult {
|
|
70
|
+
/** Number of marks spotted to the weaker player. */
|
|
71
|
+
spotMarks: number;
|
|
72
|
+
/** Whether the handicap was capped by maxHandicap. */
|
|
73
|
+
capped: boolean;
|
|
74
|
+
/** The rounding mode used ('round' | 'ceil' | 'floor'). */
|
|
75
|
+
rounding: CricketRounding;
|
|
76
|
+
/** Whether handicapPercentage compressed the spread. */
|
|
77
|
+
compressed: boolean;
|
|
78
|
+
playerA: CricketPlayerResult;
|
|
79
|
+
playerB: CricketPlayerResult;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare function ppdTo3DA(ppd: number): number;
|
|
83
|
+
declare function threeDAToPPD(tda: number): number;
|
|
84
|
+
|
|
85
|
+
declare function calculateX01Handicap(input: X01HandicapInput): X01HandicapResult;
|
|
86
|
+
|
|
87
|
+
declare function calculateCricketHandicap(input: CricketHandicapInput): CricketHandicapResult;
|
|
88
|
+
|
|
89
|
+
export { type AverageType, type CricketAverageType, type CricketHandicapInput, type CricketHandicapResult, type CricketPlayerResult, type CricketRounding, type Direction, HandicapError, type HandicapErrorCode, type PlayerAverage, type PlayerResult, type X01HandicapInput, type X01HandicapResult, calculateCricketHandicap, calculateX01Handicap, ppdTo3DA, threeDAToPPD };
|