Skip to main content

Ranking Policy

A Ranking Policy defines how points are awarded to participants for their tournament performance. It is attached to a tournament or event using the standard policy system under the key POLICY_TYPE_RANKING_POINTS.

import { POLICY_TYPE_RANKING_POINTS } from 'tods-competition-factory';

const policyDefinitions = {
[POLICY_TYPE_RANKING_POINTS]: {
awardProfiles: [...],
qualityWinProfiles: [...],
doublesAttribution: 'fullToEach',
requireWinForPoints: false,
requireWinFirstRound: true,
},
};

// Attach to tournament
tournamentEngine.attachPolicies({ policyDefinitions });

// Or pass directly
scaleEngine.getTournamentPoints({ policyDefinitions, level: 3 });

Policy Structure

{
awardProfiles: AwardProfile[]; // How points are awarded per draw/event
qualityWinProfiles?: QualityWinProfile[]; // Bonus for beating ranked opponents
doublesAttribution?: string; // 'fullToEach' | 'splitEven'
requireWinForPoints?: boolean; // Global: must win to earn position points
requireWinFirstRound?: boolean; // Global: R1 losers need a win for points
}

Award Profiles

Each awardProfile defines point values for a specific scope (draw type, level, category, etc.). When computing points, the Scale Engine selects the best-matching profile using specificity scoring.

Minimal Profile

awardProfiles: [
{
finishingPositionRanges: {
1: { value: 100 },
2: { value: 75 },
4: { value: 50 },
8: { value: 25 },
},
},
]

Full Profile

{
// Identity
profileName: 'Elimination L1-3', // For debugging/audit (shown in devContext)

// Scope — determines when this profile applies
eventTypes: ['SINGLES'], // SINGLES, DOUBLES, TEAM
drawTypes: ['SINGLE_ELIMINATION', 'FEED_IN_CHAMPIONSHIP'],
drawSizes: [32, 64], // exact draw sizes
maxDrawSize: 128, // or a maximum
levels: [1, 2, 3], // tournament levels
maxLevel: 5, // or a maximum
stages: ['MAIN'], // MAIN, QUALIFYING, CONSOLATION
flights: [1], // flight numbers
maxFlightNumber: 2, // or a maximum
dateRanges: [{ startDate: '2025-01-01', endDate: '2025-12-31' }],
participationOrder: 1, // 1 = first structure entry

// Category scope
category: {
ageCategoryCodes: ['U18'],
genders: ['MALE'],
categoryNames: ['Junior'],
categoryTypes: ['AGE'],
ratingTypes: ['WTN'],
ballTypes: ['GREEN'],
wheelchairClasses: ['QUAD'],
subTypes: ['ADVANCED'],
},

// Priority override (bypasses specificity scoring)
priority: 10,

// Position points (key = Math.max(finishingPositionRange), see Finishing Positions concept)
finishingPositionRanges: {
1: { level: { 1: 3000, 2: 1650, 3: 990 } },
2: { level: { 1: 2400, 2: 1320, 3: 792 } },
4: { level: { 1: 1800, 2: 990, 3: 594 } },
8: { level: { 1: 1200, 2: 660, 3: 396 } },
16: { level: { 1: 600, 2: 330, 3: 198 } },
32: { level: { 1: 300, 2: 165, 3: 99 } },
},

// Per-win points
perWinPoints: {
level: { 1: 300, 2: 225, 3: 150 },
},
// or flat:
pointsPerWin: 60,

// Max countable matches (per participant per draw)
maxCountableMatches: 5,
// or level-keyed:
// maxCountableMatches: { level: { 3: 5, 4: 4 } },

// Bonus points (champion/finalist)
bonusPoints: [
{ finishingPositions: [1], value: { level: { 6: 50, 7: 25 } } },
{ finishingPositions: [2], value: { level: { 6: 30, 7: 15 } } },
],

// Win requirements
requireWinForPoints: false,
requireWinFirstRound: true,
}

Position Value Resolution

The keys in finishingPositionRanges are accessors — each key equals Math.max(finishingPositionRange) for the corresponding draw round. See Finishing Positions for how these values are computed from draw structures.

Values in finishingPositionRanges can be expressed in several forms:

Simple Value

{ 1: { value: 1000 } }
// or just a number:
{ 1: 1000 }

Level-Keyed

{ 1: { level: { 1: 3000, 2: 1650, 3: 990 } } }

The level parameter passed to getTournamentPoints selects the value.

Draw Size Threshold

{ 1: [
{ threshold: 16, value: 500 },
{ threshold: 32, value: 800 },
{ threshold: 64, value: 1000 },
] }

The highest threshold <= drawSize is used.

Flight-Specific

{ 1: { flights: { 1: 1000, 2: 500 } } }

Won/Lost Accessors

{ 4: { won: 400, lost: 200 } }

Points differ based on whether the participant won a match at that finishing position (useful for consolation draws).

Per-Win Points

Per-win points are awarded for each match won, typically as an alternative to position points (when no finishingPositionRanges key matches the accessor).

Flat Value

{ pointsPerWin: 60 }

Level-Keyed

{
perWinPoints: {
level: { 1: 300, 2: 225, 3: 150 }
}
}

With Participation Order

{
perWinPoints: [
{ participationOrders: [1], level: { 1: 300, 2: 225 } }, // main draw
{ participationOrders: [2], level: { 1: 100, 2: 75 } }, // consolation
]
}

Team Line Points

For team events, per-win values can vary by line position:

{
perWinPoints: {
level: {
1: { line: [300, 275, 250, 225, 200, 175], limit: 6 }
}
}
}

The line array is indexed by collectionPosition - 1. The limit property means only the first N lines earn points.

Quality Win Profiles

Quality win profiles define bonus points for defeating ranked opponents:

qualityWinProfiles: [
{
rankingScaleName: 'NATIONAL_RANKING',
rankingSnapshot: 'tournamentStart',
unrankedOpponentBehavior: 'noBonus',
includeWalkovers: false,
maxBonusPerTournament: 500,
rankingRanges: [
{ rankRange: [1, 10], value: 225 },
{ rankRange: [11, 25], value: 203 },
{ rankRange: [26, 50], value: 169 },
],
},
]

See Quality Win Points for detailed documentation.

Doubles Attribution

Controls how pair points flow to individual participant records:

{ doublesAttribution: 'fullToEach' }  // each individual gets 100%
{ doublesAttribution: 'splitEven' } // each individual gets 50%

When not specified, points remain only on the pair record.

Specificity Scoring

When multiple profiles match a participation, the one with the most populated scope fields wins. For example:

awardProfiles: [
// Score 0: no scope constraints (catch-all)
{ finishingPositionRanges: { 1: { value: 100 } } },

// Score 3: drawTypes + levels + maxDrawSize
{ drawTypes: ['ROUND_ROBIN'], levels: [3, 4, 5], maxDrawSize: 16,
perWinPoints: { level: { 3: 225 } } },
]

The Round Robin profile (score 3) wins over the catch-all (score 0) for RR draws at levels 3-5.

To force a specific profile regardless of scoring, use priority:

{ priority: 10, drawTypes: ['ROUND_ROBIN'], ... }

See Profile Selection for the complete scoring rules.

Complete Examples

Simple Club Ranking

const clubPolicy = {
[POLICY_TYPE_RANKING_POINTS]: {
awardProfiles: [
{
finishingPositionRanges: {
1: { value: 100 },
2: { value: 75 },
4: { value: 50 },
8: { value: 25 },
},
pointsPerWin: 10,
},
],
},
};

USTA-Style Multi-Profile

const ustaPolicy = {
[POLICY_TYPE_RANKING_POINTS]: {
requireWinFirstRound: true,
doublesAttribution: 'fullToEach',
awardProfiles: [
// Elimination draws L1-3
{
profileName: 'Elimination L1-3',
drawTypes: ['SINGLE_ELIMINATION', 'FEED_IN_CHAMPIONSHIP', 'COMPASS'],
levels: [1, 2, 3],
finishingPositionRanges: {
1: { level: { 1: 3000, 2: 1650, 3: 990 } },
2: { level: { 1: 2400, 2: 1320, 3: 792 } },
4: { level: { 1: 1800, 2: 990, 3: 594 } },
8: { level: { 1: 1200, 2: 660, 3: 396 } },
16: { level: { 1: 600, 2: 330, 3: 198 } },
32: { level: { 1: 300, 2: 165, 3: 99 } },
},
},
// Round Robin L3-5 (per-win only)
{
profileName: 'Round Robin L3-5',
drawTypes: ['ROUND_ROBIN'],
levels: [3, 4, 5],
maxCountableMatches: 5,
perWinPoints: { level: { 3: 225, 4: 150, 5: 75 } },
},
],
qualityWinProfiles: [
{
rankingScaleName: 'USTA_JUNIOR',
rankingSnapshot: 'tournamentStart',
unrankedOpponentBehavior: 'noBonus',
rankingRanges: [
{ rankRange: [1, 10], value: 225 },
{ rankRange: [11, 25], value: 203 },
{ rankRange: [26, 50], value: 169 },
{ rankRange: [51, 100], value: 101 },
],
},
],
},
};

FIC Draw Policy

Feed-in consolation draws produce distinct accessor values. Map all possible positions:

{
drawTypes: ['FEED_IN_CHAMPIONSHIP'],
finishingPositionRanges: {
1: { value: 1000 },
2: { value: 700 },
3: { value: 500 }, // consolation champion
4: { value: 400 },
6: { value: 300 },
8: { value: 200 },
12: { value: 150 },
16: { value: 100 },
24: { value: 75 },
32: { value: 50 },
},
}
tip

For a 32-draw FIC, the possible accessor values are 1, 2, 3, 4, 6, 8, 12, 16, 24, 32. Participants automatically receive the best position across main and consolation structures.

Aggregation Rules

When using generateRankingList across multiple tournaments, define aggregation behavior:

aggregationRules: {
countingBuckets: [
{ bucketName: 'Singles', eventTypes: ['SINGLES'],
pointComponents: ['positionPoints', 'perWinPoints', 'bonusPoints'],
bestOfCount: 6 },
{ bucketName: 'Doubles', eventTypes: ['DOUBLES'],
pointComponents: ['positionPoints', 'perWinPoints', 'bonusPoints'],
bestOfCount: 2 },
{ bucketName: 'Quality Wins',
pointComponents: ['qualityWinPoints'],
bestOfCount: 0 },
],
rollingPeriodDays: 365,
minCountableResults: 3,
tiebreakCriteria: ['highestSingleResult', 'mostWins'],
}

See Multi-Tournament Aggregation for detailed documentation.