@opprs/core 0.4.0 → 0.5.2-canary.8e722bf
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 +21 -649
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @opprs/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Configurable Constants** - Override any calculation constant to customize the ranking system
|
|
8
|
-
- **Base Value Calculation** - Tournament value based on number of rated players
|
|
9
|
-
- **Tournament Value Adjustment (TVA)** - Strength indicators from player ratings and rankings
|
|
10
|
-
- **Tournament Grading Percentage (TGP)** - Format quality assessment
|
|
11
|
-
- **Event Boosters** - Multipliers for major championships and certified events
|
|
12
|
-
- **Point Distribution** - Linear and dynamic point allocation
|
|
13
|
-
- **Time Decay** - Automatic point depreciation over time
|
|
14
|
-
- **Glicko Rating System** - Player skill rating with uncertainty
|
|
15
|
-
- **Efficiency Tracking** - Performance metrics
|
|
16
|
-
- **Input Validation** - Comprehensive data validation
|
|
17
|
-
- **TypeScript First** - Full type safety and IntelliSense support
|
|
3
|
+
Core TypeScript library for the Open Pinball Player Ranking System (OPPRS). Provides all calculation functions for tournament rankings and player ratings.
|
|
18
4
|
|
|
19
5
|
## Installation
|
|
20
6
|
|
|
21
7
|
```bash
|
|
22
|
-
npm install
|
|
8
|
+
npm install @opprs/core
|
|
23
9
|
```
|
|
24
10
|
|
|
25
11
|
## Quick Start
|
|
@@ -35,31 +21,24 @@ import {
|
|
|
35
21
|
type Player,
|
|
36
22
|
type TGPConfig,
|
|
37
23
|
type PlayerResult,
|
|
38
|
-
} from '
|
|
24
|
+
} from '@opprs/core';
|
|
39
25
|
|
|
40
|
-
// Define
|
|
26
|
+
// Define players
|
|
41
27
|
const players: Player[] = [
|
|
42
28
|
{ id: '1', rating: 1800, ranking: 1, isRated: true },
|
|
43
29
|
{ id: '2', rating: 1700, ranking: 5, isRated: true },
|
|
44
30
|
{ id: '3', rating: 1600, ranking: 10, isRated: true },
|
|
45
31
|
];
|
|
46
32
|
|
|
47
|
-
// Calculate tournament value
|
|
33
|
+
// Calculate tournament value components
|
|
48
34
|
const baseValue = calculateBaseValue(players);
|
|
49
35
|
const ratingTVA = calculateRatingTVA(players);
|
|
50
36
|
const rankingTVA = calculateRankingTVA(players);
|
|
51
37
|
|
|
52
38
|
// Configure tournament format
|
|
53
39
|
const tgpConfig: TGPConfig = {
|
|
54
|
-
qualifying: {
|
|
55
|
-
|
|
56
|
-
meaningfulGames: 7,
|
|
57
|
-
},
|
|
58
|
-
finals: {
|
|
59
|
-
formatType: 'match-play',
|
|
60
|
-
meaningfulGames: 12,
|
|
61
|
-
fourPlayerGroups: true, // PAPA-style 4-player groups
|
|
62
|
-
},
|
|
40
|
+
qualifying: { type: 'limited', meaningfulGames: 7 },
|
|
41
|
+
finals: { formatType: 'match-play', meaningfulGames: 12, fourPlayerGroups: true },
|
|
63
42
|
};
|
|
64
43
|
|
|
65
44
|
const tgp = calculateTGP(tgpConfig);
|
|
@@ -68,7 +47,7 @@ const eventBooster = getEventBoosterMultiplier('none');
|
|
|
68
47
|
// Calculate first place value
|
|
69
48
|
const firstPlaceValue = (baseValue + ratingTVA + rankingTVA) * tgp * eventBooster;
|
|
70
49
|
|
|
71
|
-
// Distribute points
|
|
50
|
+
// Distribute points based on finishing positions
|
|
72
51
|
const results: PlayerResult[] = [
|
|
73
52
|
{ player: players[0], position: 1 },
|
|
74
53
|
{ player: players[1], position: 2 },
|
|
@@ -76,630 +55,23 @@ const results: PlayerResult[] = [
|
|
|
76
55
|
];
|
|
77
56
|
|
|
78
57
|
const distributions = distributePoints(results, firstPlaceValue);
|
|
79
|
-
|
|
80
|
-
console.log(`First place gets: ${distributions[0].totalPoints.toFixed(2)} points`);
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Configuration
|
|
84
|
-
|
|
85
|
-
The OPPR library allows you to configure all calculation constants to customize the ranking system for your specific needs. By default, the library uses the standard OPPR constants, but you can override any value globally.
|
|
86
|
-
|
|
87
|
-
### Basic Configuration
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
import { configureOPPR, calculateBaseValue } from 'oppr';
|
|
91
|
-
|
|
92
|
-
// Configure specific constants
|
|
93
|
-
configureOPPR({
|
|
94
|
-
BASE_VALUE: {
|
|
95
|
-
POINTS_PER_PLAYER: 1.0, // Override: changed from default 0.5
|
|
96
|
-
MAX_BASE_VALUE: 64, // Override: changed from default 32
|
|
97
|
-
},
|
|
98
|
-
TIME_DECAY: {
|
|
99
|
-
YEAR_1_TO_2: 0.8, // Override: changed from default 0.75
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// All function calls now use your configured values
|
|
104
|
-
const baseValue = calculateBaseValue(players);
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Configuration Options
|
|
108
|
-
|
|
109
|
-
You can configure any of the following constant groups:
|
|
110
|
-
|
|
111
|
-
- `BASE_VALUE` - Tournament base value calculation
|
|
112
|
-
- `TVA` - Tournament Value Adjustment (rating and ranking)
|
|
113
|
-
- `TGP` - Tournament Grading Percentage
|
|
114
|
-
- `EVENT_BOOSTERS` - Event multipliers
|
|
115
|
-
- `POINT_DISTRIBUTION` - Point allocation
|
|
116
|
-
- `TIME_DECAY` - Point depreciation
|
|
117
|
-
- `RANKING` - Player ranking rules
|
|
118
|
-
- `RATING` - Glicko rating system
|
|
119
|
-
- `VALIDATION` - Input validation rules
|
|
120
|
-
|
|
121
|
-
### Partial Configuration
|
|
122
|
-
|
|
123
|
-
You only need to specify the values you want to override. All other values will use the defaults:
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
import { configureOPPR } from 'oppr';
|
|
127
|
-
|
|
128
|
-
// Only override specific nested values
|
|
129
|
-
configureOPPR({
|
|
130
|
-
TVA: {
|
|
131
|
-
RATING: {
|
|
132
|
-
MAX_VALUE: 30, // Only override this one value
|
|
133
|
-
// All other TVA.RATING values use defaults
|
|
134
|
-
},
|
|
135
|
-
// TVA.RANKING uses all defaults
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Resetting Configuration
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
import { resetConfig } from 'oppr';
|
|
144
|
-
|
|
145
|
-
// Reset all constants back to defaults
|
|
146
|
-
resetConfig();
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Accessing Default Constants
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
import { getDefaultConfig, DEFAULT_CONSTANTS } from 'oppr';
|
|
153
|
-
|
|
154
|
-
// Get the current defaults programmatically
|
|
155
|
-
const defaults = getDefaultConfig();
|
|
156
|
-
console.log(defaults.BASE_VALUE.POINTS_PER_PLAYER); // 0.5
|
|
157
|
-
|
|
158
|
-
// Or access the constant object directly
|
|
159
|
-
console.log(DEFAULT_CONSTANTS.BASE_VALUE.MAX_BASE_VALUE); // 32
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Configuration Examples
|
|
163
|
-
|
|
164
|
-
#### Example 1: Higher Tournament Values
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import { configureOPPR } from 'oppr';
|
|
168
|
-
|
|
169
|
-
// Make tournaments worth more points
|
|
170
|
-
configureOPPR({
|
|
171
|
-
BASE_VALUE: {
|
|
172
|
-
POINTS_PER_PLAYER: 1.0, // Double from 0.5
|
|
173
|
-
MAX_BASE_VALUE: 64, // Double from 32
|
|
174
|
-
},
|
|
175
|
-
TVA: {
|
|
176
|
-
RATING: {
|
|
177
|
-
MAX_VALUE: 50, // Double from 25
|
|
178
|
-
},
|
|
179
|
-
RANKING: {
|
|
180
|
-
MAX_VALUE: 100, // Double from 50
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
#### Example 2: Slower Time Decay
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
import { configureOPPR } from 'oppr';
|
|
190
|
-
|
|
191
|
-
// Keep points valuable longer
|
|
192
|
-
configureOPPR({
|
|
193
|
-
TIME_DECAY: {
|
|
194
|
-
YEAR_0_TO_1: 1.0, // Default
|
|
195
|
-
YEAR_1_TO_2: 0.9, // Changed from 0.75
|
|
196
|
-
YEAR_2_TO_3: 0.7, // Changed from 0.5
|
|
197
|
-
YEAR_3_PLUS: 0.5, // Changed from 0.0
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
#### Example 3: Different TGP Scaling
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import { configureOPPR } from 'oppr';
|
|
206
|
-
|
|
207
|
-
// Adjust TGP values
|
|
208
|
-
configureOPPR({
|
|
209
|
-
TGP: {
|
|
210
|
-
BASE_GAME_VALUE: 0.05, // 5% per game instead of 4%
|
|
211
|
-
MAX_WITHOUT_FINALS: 1.5, // 150% max instead of 100%
|
|
212
|
-
MAX_WITH_FINALS: 2.5, // 250% max instead of 200%
|
|
213
|
-
MULTIPLIERS: {
|
|
214
|
-
FOUR_PLAYER_GROUPS: 2.5, // Higher multiplier
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### TypeScript Support
|
|
221
|
-
|
|
222
|
-
The configuration system is fully typed. Your IDE will provide autocomplete and type checking:
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
import { configureOPPR, type PartialOPPRConfig } from 'oppr';
|
|
226
|
-
|
|
227
|
-
const myConfig: PartialOPPRConfig = {
|
|
228
|
-
BASE_VALUE: {
|
|
229
|
-
POINTS_PER_PLAYER: 1.0,
|
|
230
|
-
// TypeScript will show all available options
|
|
231
|
-
// and catch any typos or invalid values
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
configureOPPR(myConfig);
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Core Concepts
|
|
239
|
-
|
|
240
|
-
### Base Value
|
|
241
|
-
|
|
242
|
-
The base value of a tournament is calculated based on the number of rated players (players with 5+ events):
|
|
243
|
-
|
|
244
|
-
- **0.5 points per rated player**
|
|
245
|
-
- **Maximum of 32 points** (achieved at 64+ rated players)
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
import { calculateBaseValue } from 'oppr';
|
|
249
|
-
|
|
250
|
-
const baseValue = calculateBaseValue(players);
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Tournament Value Adjustment (TVA)
|
|
254
|
-
|
|
255
|
-
TVA increases tournament value based on the strength of the field:
|
|
256
|
-
|
|
257
|
-
#### Rating-based TVA
|
|
258
|
-
- Uses Glicko ratings to assess player skill
|
|
259
|
-
- Maximum contribution: **25 points**
|
|
260
|
-
- Formula: `(rating * 0.000546875) - 0.703125`
|
|
261
|
-
|
|
262
|
-
#### Ranking-based TVA
|
|
263
|
-
- Uses world rankings to assess field strength
|
|
264
|
-
- Maximum contribution: **50 points**
|
|
265
|
-
- Formula: `ln(ranking) * -0.211675054 + 1.459827968`
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
import { calculateRatingTVA, calculateRankingTVA } from 'oppr';
|
|
269
|
-
|
|
270
|
-
const ratingTVA = calculateRatingTVA(players);
|
|
271
|
-
const rankingTVA = calculateRankingTVA(players);
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Tournament Grading Percentage (TGP)
|
|
275
|
-
|
|
276
|
-
TGP measures the quality and completeness of a tournament format:
|
|
277
|
-
|
|
278
|
-
- **Base value:** 4% per meaningful game
|
|
279
|
-
- **Without separate qualifying:** Max 100%
|
|
280
|
-
- **With qualifying and finals:** Max 200%
|
|
281
|
-
- **Multipliers:**
|
|
282
|
-
- 4-player groups: 2X
|
|
283
|
-
- 3-player groups: 1.5X
|
|
284
|
-
- Unlimited best game (20+ hours): 2X
|
|
285
|
-
- Hybrid best game: 3X
|
|
286
|
-
- Unlimited card qualifying: 4X
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
import { calculateTGP, type TGPConfig } from 'oppr';
|
|
290
|
-
|
|
291
|
-
const tgpConfig: TGPConfig = {
|
|
292
|
-
qualifying: {
|
|
293
|
-
type: 'limited',
|
|
294
|
-
meaningfulGames: 7,
|
|
295
|
-
},
|
|
296
|
-
finals: {
|
|
297
|
-
formatType: 'match-play',
|
|
298
|
-
meaningfulGames: 15,
|
|
299
|
-
fourPlayerGroups: true,
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const tgp = calculateTGP(tgpConfig);
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Event Boosters
|
|
307
|
-
|
|
308
|
-
Event boosters multiply the final tournament value:
|
|
309
|
-
|
|
310
|
-
- **None:** 1.0X (100%)
|
|
311
|
-
- **Certified:** 1.25X (125%)
|
|
312
|
-
- **Certified+:** 1.5X (150%)
|
|
313
|
-
- **Championship Series:** 1.5X (150%)
|
|
314
|
-
- **Major:** 2.0X (200%)
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
import { getEventBoosterMultiplier } from 'oppr';
|
|
318
|
-
|
|
319
|
-
const booster = getEventBoosterMultiplier('major'); // Returns 2.0
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Point Distribution
|
|
323
|
-
|
|
324
|
-
Points are distributed using two components:
|
|
325
|
-
|
|
326
|
-
1. **Linear Distribution (10%):** Evenly distributed across positions
|
|
327
|
-
2. **Dynamic Distribution (90%):** Heavily weighted toward top finishers
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { distributePoints } from 'oppr';
|
|
331
|
-
|
|
332
|
-
const distributions = distributePoints(results, firstPlaceValue);
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Time Decay
|
|
336
|
-
|
|
337
|
-
Points decay over time to emphasize recent performance:
|
|
338
|
-
|
|
339
|
-
- **0-1 years:** 100% value
|
|
340
|
-
- **1-2 years:** 75% value
|
|
341
|
-
- **2-3 years:** 50% value
|
|
342
|
-
- **3+ years:** 0% value (inactive)
|
|
343
|
-
|
|
344
|
-
```typescript
|
|
345
|
-
import { applyTimeDecay, isEventActive } from 'oppr';
|
|
346
|
-
|
|
347
|
-
const eventDate = new Date('2023-01-01');
|
|
348
|
-
const decayedPoints = applyTimeDecay(100, eventDate);
|
|
349
|
-
const active = isEventActive(eventDate);
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Glicko Rating System
|
|
353
|
-
|
|
354
|
-
Player ratings use the Glicko system with rating deviation (uncertainty):
|
|
355
|
-
|
|
356
|
-
- **Default rating:** 1300
|
|
357
|
-
- **Rating deviation (RD):** 10-200
|
|
358
|
-
- **RD decay:** ~0.3 per day of inactivity
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
import { updateRating, type RatingUpdate } from 'oppr';
|
|
362
|
-
|
|
363
|
-
const update: RatingUpdate = {
|
|
364
|
-
currentRating: 1500,
|
|
365
|
-
currentRD: 100,
|
|
366
|
-
results: [
|
|
367
|
-
{ opponentRating: 1600, opponentRD: 80, score: 1 }, // Win
|
|
368
|
-
{ opponentRating: 1550, opponentRD: 90, score: 0 }, // Loss
|
|
369
|
-
],
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const { newRating, newRD } = updateRating(update);
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
## Constants and Calibration Rationale
|
|
376
|
-
|
|
377
|
-
This section explains why each constant in the system has its current value. The constants are carefully calibrated to create a balanced, mathematically sound ranking system.
|
|
378
|
-
|
|
379
|
-
### Design Philosophy
|
|
380
|
-
|
|
381
|
-
Most constants are chosen to:
|
|
382
|
-
1. **Cap maximum contributions** at specific thresholds
|
|
383
|
-
2. **Create mathematical relationships** between different components
|
|
384
|
-
3. **Follow established rating systems** (like Glicko)
|
|
385
|
-
4. **Reflect real-world competitive difficulty**
|
|
386
|
-
|
|
387
|
-
Many constants are interdependent - changing one often requires adjusting others to maintain system balance.
|
|
388
|
-
|
|
389
|
-
### Base Value Constants
|
|
390
|
-
|
|
391
|
-
| Constant | Value | Rationale |
|
|
392
|
-
|----------|-------|-----------|
|
|
393
|
-
| `POINTS_PER_PLAYER` | `0.5` | Chosen so 64 rated players yields exactly 32 points (0.5 × 64 = 32) |
|
|
394
|
-
| `MAX_BASE_VALUE` | `32` | Reasonable cap for tournament base value |
|
|
395
|
-
| `MAX_PLAYER_COUNT` | `64` | Player count where max is reached (32 ÷ 0.5 = 64) |
|
|
396
|
-
| `RATED_PLAYER_THRESHOLD` | `5` | Five events provides sufficient history to be "rated" |
|
|
397
|
-
|
|
398
|
-
**Key Insight:** The 0.5 coefficient creates perfect linear scaling where the cap is reached at a reasonable tournament size.
|
|
399
|
-
|
|
400
|
-
### Tournament Value Adjustment (TVA) Constants
|
|
401
|
-
|
|
402
|
-
#### Rating-based TVA
|
|
403
|
-
|
|
404
|
-
| Constant | Value | Rationale |
|
|
405
|
-
|----------|-------|-----------|
|
|
406
|
-
| `MAX_VALUE` | `25` | Ensures rating TVA contributes 1/3 of the 75-point total TVA cap |
|
|
407
|
-
| `COEFFICIENT` | `0.000546875` | Reverse-engineered so 64 players rated 2000 contribute exactly 25 points |
|
|
408
|
-
| `OFFSET` | `0.703125` | Paired with coefficient: `(2000 × 0.000546875) - 0.703125 ≈ 0.39` per player |
|
|
409
|
-
| `PERFECT_RATING` | `2000` | Reference rating for "perfect" player |
|
|
410
|
-
| `MIN_EFFECTIVE_RATING` | `1285.71` | Where formula crosses zero: `(1285.71 × 0.000546875) - 0.703125 ≈ 0` |
|
|
411
|
-
|
|
412
|
-
**Formula:** `(rating * 0.000546875) - 0.703125`
|
|
413
|
-
|
|
414
|
-
The coefficients ensure 64 perfect players contribute exactly 25 points: 64 × 0.39 ≈ 25 ✓
|
|
415
|
-
|
|
416
|
-
#### Ranking-based TVA
|
|
417
|
-
|
|
418
|
-
| Constant | Value | Rationale |
|
|
419
|
-
|----------|-------|-----------|
|
|
420
|
-
| `MAX_VALUE` | `50` | Largest component of TVA (2/3 of 75-point cap) |
|
|
421
|
-
| `COEFFICIENT` | `-0.211675054` | Calibrated so top 64 ranked players sum to exactly 50 points |
|
|
422
|
-
| `OFFSET` | `1.459827968` | Creates logarithmic decay favoring top-ranked players |
|
|
423
|
-
|
|
424
|
-
**Formula:** `ln(ranking) * -0.211675054 + 1.459827968`
|
|
425
|
-
|
|
426
|
-
- Rank #1: ~1.46 points
|
|
427
|
-
- Rank #2: ~1.31 points
|
|
428
|
-
- Sum of ranks 1-64: ~50 points
|
|
429
|
-
|
|
430
|
-
**Key Insight:** The logarithmic formula heavily rewards top-ranked players, while the rating formula is more linear.
|
|
431
|
-
|
|
432
|
-
#### General TVA
|
|
433
|
-
|
|
434
|
-
| Constant | Value | Rationale |
|
|
435
|
-
|----------|-------|-----------|
|
|
436
|
-
| `MAX_PLAYERS_CONSIDERED` | `64` | Limits calculation scope; prevents diminishing returns from large fields |
|
|
437
|
-
|
|
438
|
-
### TGP (Tournament Grading Percentage) Constants
|
|
439
|
-
|
|
440
|
-
#### Base Values
|
|
441
|
-
|
|
442
|
-
| Constant | Value | Rationale |
|
|
443
|
-
|----------|-------|-----------|
|
|
444
|
-
| `BASE_GAME_VALUE` | `0.04` (4%) | Base unit chosen so 25 meaningful games = 100% TGP |
|
|
445
|
-
| `MAX_WITHOUT_FINALS` | `1.0` (100%) | Standard cap for simple tournaments |
|
|
446
|
-
| `MAX_WITH_FINALS` | `2.0` (200%) | Allows qualifying + finals to each contribute up to 100% |
|
|
447
|
-
| `MAX_GAMES_FOR_200_PERCENT` | `50` | 50 games × 4% = 200% (matches the math) |
|
|
448
|
-
|
|
449
|
-
#### Format Multipliers
|
|
450
|
-
|
|
451
|
-
These reflect **competitive difficulty**:
|
|
452
|
-
|
|
453
|
-
| Format | Multiplier | Effective % | Rationale |
|
|
454
|
-
|--------|------------|-------------|-----------|
|
|
455
|
-
| Four-player groups | `2.0` | 8% per game | Most competitive format (PAPA-style) |
|
|
456
|
-
| Three-player groups | `1.5` | 6% per game | Less competitive than 4-player |
|
|
457
|
-
| Unlimited best game | `2.0` | 8% per game | Requires 20+ hours of qualifying |
|
|
458
|
-
| Hybrid best game | `3.0` | 12% per game | Combines multiple competitive elements |
|
|
459
|
-
| Unlimited card | `4.0` | 16% per game | Highest difficulty: unlimited practice + card format |
|
|
460
|
-
|
|
461
|
-
**Key Insight:** Higher multipliers = harder formats = more TGP value per game
|
|
462
|
-
|
|
463
|
-
#### Ball Count Adjustments
|
|
464
|
-
|
|
465
|
-
| Ball Count | Multiplier | Rationale |
|
|
466
|
-
|------------|------------|-----------|
|
|
467
|
-
| 1-ball | `0.33` (33%) | Less meaningful competition than standard 3-ball |
|
|
468
|
-
| 2-ball | `0.66` (66%) | Linear scaling between 1 and 3-ball |
|
|
469
|
-
| 3+ ball | `1.0` (100%) | Standard competitive format |
|
|
470
|
-
|
|
471
|
-
#### Unlimited Qualifying
|
|
472
|
-
|
|
473
|
-
| Constant | Value | Rationale |
|
|
474
|
-
|----------|-------|-----------|
|
|
475
|
-
| `PERCENT_PER_HOUR` | `0.01` (1%) | Rewards longer qualifying periods |
|
|
476
|
-
| `MAX_BONUS` | `0.2` (20%) | Caps at 20% bonus (achieved at 20 hours) |
|
|
477
|
-
| `MIN_HOURS_FOR_MULTIPLIER` | `20` | Must run 20+ hours to qualify for format multipliers |
|
|
478
|
-
|
|
479
|
-
#### Finals Requirements
|
|
480
|
-
|
|
481
|
-
| Constant | Value | Rationale |
|
|
482
|
-
|----------|-------|-----------|
|
|
483
|
-
| `MIN_FINALISTS_PERCENT` | `0.1` (10%) | At least 10% must advance to ensure finals are meaningful |
|
|
484
|
-
| `MAX_FINALISTS_PERCENT` | `0.5` (50%) | Maximum 50% prevents finals from being too inclusive |
|
|
485
|
-
|
|
486
|
-
### Event Booster Constants
|
|
487
|
-
|
|
488
|
-
| Booster Type | Multiplier | Rationale |
|
|
489
|
-
|--------------|------------|-----------|
|
|
490
|
-
| None | `1.0` (100%) | Standard events, no adjustment |
|
|
491
|
-
| Certified | `1.25` (125%) | 25% boost for meeting certification requirements (24+ finalists, valid format) |
|
|
492
|
-
| Certified+ | `1.5` (150%) | 50% boost requires 128+ rated players |
|
|
493
|
-
| Championship Series | `1.5` (150%) | Same as Certified+ for series events |
|
|
494
|
-
| Major | `2.0` (200%) | 100% boost doubles the value of major championships |
|
|
495
|
-
|
|
496
|
-
**Key Insight:** These create tiers that incentivize higher-quality tournaments.
|
|
497
|
-
|
|
498
|
-
### Point Distribution Constants
|
|
499
|
-
|
|
500
|
-
| Constant | Value | Rationale |
|
|
501
|
-
|----------|-------|-----------|
|
|
502
|
-
| `LINEAR_PERCENTAGE` | `0.1` (10%) | Everyone gets some points |
|
|
503
|
-
| `DYNAMIC_PERCENTAGE` | `0.9` (90%) | Heavily rewards top finishers |
|
|
504
|
-
| `POSITION_EXPONENT` | `0.7` | Creates curve less steep than linear but more aggressive than logarithmic |
|
|
505
|
-
| `VALUE_EXPONENT` | `3` | Cubic function creates exponential decay from 1st to last place |
|
|
506
|
-
| `MAX_DYNAMIC_PLAYERS` | `64` | Caps denominator so small tournaments don't over-penalize lower finishers |
|
|
507
|
-
|
|
508
|
-
**Formula:**
|
|
509
|
-
```typescript
|
|
510
|
-
power((1 - power(((Position - 1) / min(RatedPlayerCount/2, 64)), 0.7)), 3) * 0.9 * FirstPlaceValue
|
|
511
58
|
```
|
|
512
59
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
### Time Decay Constants
|
|
516
|
-
|
|
517
|
-
| Time Period | Multiplier | Rationale |
|
|
518
|
-
|-------------|------------|-----------|
|
|
519
|
-
| 0-1 years | `1.0` (100%) | Recent performance at full value |
|
|
520
|
-
| 1-2 years | `0.75` (75%) | 25% annual decay begins |
|
|
521
|
-
| 2-3 years | `0.5` (50%) | Continues progressive decay |
|
|
522
|
-
| 3+ years | `0.0` (0%) | Complete removal after 3 years |
|
|
523
|
-
|
|
524
|
-
| Constant | Value | Rationale |
|
|
525
|
-
|----------|-------|-----------|
|
|
526
|
-
| `DAYS_PER_YEAR` | `365` | Standard year length for calculations |
|
|
527
|
-
|
|
528
|
-
**Key Insight:** The 3-year window and 25% annual decay steps are standard in ranking systems, emphasizing recent performance while gradually phasing out older results.
|
|
529
|
-
|
|
530
|
-
### Ranking System Constants
|
|
531
|
-
|
|
532
|
-
| Constant | Value | Rationale |
|
|
533
|
-
|----------|-------|-----------|
|
|
534
|
-
| `TOP_EVENTS_COUNT` | `15` | Top 15 events count toward ranking (similar to IFPA) |
|
|
535
|
-
| `ENTRY_RANKING_PERCENTILE` | `0.1` (10th) | New players start at 10th percentile (reasonable pessimistic assumption) |
|
|
536
|
-
|
|
537
|
-
### Glicko Rating System Constants
|
|
538
|
-
|
|
539
|
-
| Constant | Value | Rationale |
|
|
540
|
-
|----------|-------|-----------|
|
|
541
|
-
| `DEFAULT_RATING` | `1300` | Standard Glicko starting rating (slightly below average) |
|
|
542
|
-
| `MIN_RD` | `10` | Minimum uncertainty for highly active players |
|
|
543
|
-
| `MAX_RD` | `200` | Maximum uncertainty (new/inactive players) |
|
|
544
|
-
| `RD_DECAY_PER_DAY` | `0.3` | ~90 days of inactivity returns to max uncertainty (0.3 × 300 ≈ 90) |
|
|
545
|
-
| `OPPONENTS_RANGE` | `32` | Limits calculation to 32 players above/below (performance optimization) |
|
|
546
|
-
| `Q` | `Math.LN10 / 400` | **Mathematical constant from Glicko formula** (≈ 0.00575646) |
|
|
547
|
-
|
|
548
|
-
**Key Insight:** These are standard Glicko parameters based on Mark Glickman's research, not arbitrary choices. The Q value is a mathematical constant: `ln(10) / 400`.
|
|
549
|
-
|
|
550
|
-
### Validation Constants
|
|
551
|
-
|
|
552
|
-
| Constant | Value | Rationale |
|
|
553
|
-
|----------|-------|-----------|
|
|
554
|
-
| `MIN_PLAYERS` | `3` | Absolute minimum for competitive validity |
|
|
555
|
-
| `MIN_PRIVATE_PLAYERS` | `16` | Higher bar for private tournaments |
|
|
556
|
-
| `MAX_GAMES_PER_MACHINE` | `3` | Prevents over-reliance on single machines |
|
|
557
|
-
| `MIN_PARTICIPATION_PERCENT` | `0.5` (50%) | Data quality threshold for including results |
|
|
558
|
-
|
|
559
|
-
### Mathematical Interdependencies
|
|
560
|
-
|
|
561
|
-
Several constants are **mathematically linked**:
|
|
562
|
-
|
|
563
|
-
1. **Base Value:** `0.5 × 64 = 32` (points per player × max players = max value)
|
|
564
|
-
2. **Rating TVA:** Coefficients ensure 64 perfect players = 25 points
|
|
565
|
-
3. **Ranking TVA:** Logarithmic coefficients ensure top 64 = 50 points
|
|
566
|
-
4. **TGP:** `0.04 × 50 = 2.0` (base value × max games = max TGP)
|
|
567
|
-
5. **Glicko Q:** `ln(10) / 400` is a mathematical constant, not arbitrary
|
|
568
|
-
|
|
569
|
-
**Warning:** The system is highly calibrated. Changing one constant often requires adjusting others to maintain balance.
|
|
570
|
-
|
|
571
|
-
### Summary
|
|
572
|
-
|
|
573
|
-
Most constants fall into three categories:
|
|
574
|
-
|
|
575
|
-
1. **Mathematical calibrations** (TVA coefficients, Glicko Q) - Derived from formulas
|
|
576
|
-
2. **Empirical balance tuning** (TGP multipliers, point distribution exponents) - Adjusted to feel "fair"
|
|
577
|
-
3. **Standard values** (Glicko defaults, 3-year decay) - Industry best practices
|
|
578
|
-
|
|
579
|
-
Together, these constants create a comprehensive ranking system where tournament value scales appropriately with field strength, format difficulty, and competitive level.
|
|
580
|
-
|
|
581
|
-
## API Reference
|
|
582
|
-
|
|
583
|
-
### Types
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
interface Player {
|
|
587
|
-
id: string;
|
|
588
|
-
rating: number;
|
|
589
|
-
ranking: number;
|
|
590
|
-
isRated: boolean;
|
|
591
|
-
ratingDeviation?: number;
|
|
592
|
-
eventCount?: number;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
interface TGPConfig {
|
|
596
|
-
qualifying: {
|
|
597
|
-
type: 'unlimited' | 'limited' | 'hybrid' | 'none';
|
|
598
|
-
meaningfulGames: number;
|
|
599
|
-
hours?: number;
|
|
600
|
-
fourPlayerGroups?: boolean;
|
|
601
|
-
threePlayerGroups?: boolean;
|
|
602
|
-
multiMatchplay?: boolean;
|
|
603
|
-
};
|
|
604
|
-
finals: {
|
|
605
|
-
formatType: TournamentFormatType;
|
|
606
|
-
meaningfulGames: number;
|
|
607
|
-
fourPlayerGroups?: boolean;
|
|
608
|
-
threePlayerGroups?: boolean;
|
|
609
|
-
};
|
|
610
|
-
ballCountAdjustment?: number;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
interface PlayerResult {
|
|
614
|
-
player: Player;
|
|
615
|
-
position: number;
|
|
616
|
-
optedOut?: boolean;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
interface PointDistribution {
|
|
620
|
-
player: Player;
|
|
621
|
-
position: number;
|
|
622
|
-
linearPoints: number;
|
|
623
|
-
dynamicPoints: number;
|
|
624
|
-
totalPoints: number;
|
|
625
|
-
}
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
### Functions
|
|
629
|
-
|
|
630
|
-
#### Base Value
|
|
631
|
-
- `calculateBaseValue(players: Player[]): number`
|
|
632
|
-
- `countRatedPlayers(players: Player[]): number`
|
|
633
|
-
- `isPlayerRated(eventCount: number): boolean`
|
|
634
|
-
|
|
635
|
-
#### TVA
|
|
636
|
-
- `calculateRatingTVA(players: Player[]): number`
|
|
637
|
-
- `calculateRankingTVA(players: Player[]): number`
|
|
638
|
-
- `calculateTotalTVA(players: Player[]): { ratingTVA, rankingTVA, totalTVA }`
|
|
639
|
-
|
|
640
|
-
#### TGP
|
|
641
|
-
- `calculateTGP(config: TGPConfig): number`
|
|
642
|
-
- `calculateQualifyingTGP(config: TGPConfig): number`
|
|
643
|
-
- `calculateFinalsTGP(config: TGPConfig): number`
|
|
644
|
-
|
|
645
|
-
#### Event Boosters
|
|
646
|
-
- `getEventBoosterMultiplier(type: EventBoosterType): number`
|
|
647
|
-
- `qualifiesForCertified(...): boolean`
|
|
648
|
-
- `qualifiesForCertifiedPlus(...): boolean`
|
|
649
|
-
|
|
650
|
-
#### Point Distribution
|
|
651
|
-
- `distributePoints(results: PlayerResult[], firstPlaceValue: number): PointDistribution[]`
|
|
652
|
-
- `calculatePlayerPoints(position, playerCount, ratedPlayerCount, firstPlaceValue): number`
|
|
653
|
-
|
|
654
|
-
#### Time Decay
|
|
655
|
-
- `applyTimeDecay(points: number, eventDate: Date): number`
|
|
656
|
-
- `isEventActive(eventDate: Date): boolean`
|
|
657
|
-
- `getDecayMultiplier(ageInYears: number): number`
|
|
658
|
-
|
|
659
|
-
#### Rating
|
|
660
|
-
- `updateRating(update: RatingUpdate): RatingResult`
|
|
661
|
-
- `simulateTournamentMatches(position, results): MatchResult[]`
|
|
662
|
-
|
|
663
|
-
#### Efficiency
|
|
664
|
-
- `calculateOverallEfficiency(events: PlayerEvent[]): number`
|
|
665
|
-
- `getEfficiencyStats(events: PlayerEvent[]): EfficiencyStats`
|
|
666
|
-
|
|
667
|
-
## Development
|
|
668
|
-
|
|
669
|
-
```bash
|
|
670
|
-
# Install dependencies
|
|
671
|
-
npm install
|
|
672
|
-
|
|
673
|
-
# Run tests
|
|
674
|
-
npm test
|
|
675
|
-
|
|
676
|
-
# Run tests with coverage
|
|
677
|
-
npm run test:coverage
|
|
678
|
-
|
|
679
|
-
# Build the library
|
|
680
|
-
npm run build
|
|
60
|
+
## Features
|
|
681
61
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
62
|
+
- **Base Value** - Tournament value based on rated player count
|
|
63
|
+
- **TVA (Tournament Value Adjustment)** - Field strength from ratings and rankings
|
|
64
|
+
- **TGP (Tournament Grading Percentage)** - Format quality assessment
|
|
65
|
+
- **Event Boosters** - Multipliers for majors and championships
|
|
66
|
+
- **Point Distribution** - Linear and dynamic point allocation
|
|
67
|
+
- **Time Decay** - Point depreciation over time
|
|
68
|
+
- **Glicko Ratings** - Player skill ratings with uncertainty
|
|
69
|
+
- **Configuration** - Override any calculation constant via `configureOPPR()`
|
|
686
70
|
|
|
687
|
-
##
|
|
71
|
+
## Documentation
|
|
688
72
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
```bash
|
|
692
|
-
npm run test:coverage
|
|
693
|
-
```
|
|
73
|
+
For detailed documentation including API reference, configuration options, and calibration rationale, see the [OPPRS Documentation](https://thatguyinabeanie.github.io/OPPR/).
|
|
694
74
|
|
|
695
75
|
## License
|
|
696
76
|
|
|
697
|
-
MIT
|
|
698
|
-
|
|
699
|
-
## Contributing
|
|
700
|
-
|
|
701
|
-
Contributions are welcome! Please ensure all tests pass and maintain the existing code style.
|
|
702
|
-
|
|
703
|
-
## Acknowledgments
|
|
704
|
-
|
|
705
|
-
This library implements a ranking system based on tournament ranking principles for competitive pinball events.
|
|
77
|
+
MIT
|
package/package.json
CHANGED