Round Robin Tally Policy
The Round Robin Tally Policy controls how participants are ranked within round robin groups, including tiebreaking procedures, head-to-head calculations, and group order determination.
Overview
Round robin groups require sophisticated tiebreaking rules to determine final standings when participants have equal records. The tally policy provides:
- Flexible tiebreaking through configurable
tallyDirectives - Head-to-head comparisons between tied participants
- Percentage calculations (matches, sets, games, points)
- Statistical tracking (wins, losses, defaults, retirements)
- Threshold-based rules via maxParticipants attribute
- Composite scoring through GEMscore
- Group totals for percentage calculations
Default Behavior
When no custom tally policy is specified, the system applies a comprehensive default tiebreaking procedure that follows widely-accepted tournament standards.
Standard Tiebreaking Sequence
The default tally policy implements the following hierarchy:
- Primary criterion: Participants grouped by
matchUpsWon - Head-to-head comparison: When participants are tied, their direct results against each other are evaluated
- Percentage-based tiebreakers: Match, set, game, and point percentages (in order)
- Scope escalation: First evaluate all group matches, then recalculate among tied participants only
Algorithm Overview
The tiebreaking algorithm works as follows:
- Group participants by the primary attribute (default:
matchUpsWon) - For each tied group:
- If only 1 participant: resolved (no tie)
- If exactly 2 participants: compare head-to-head record
- If 3+ participants: apply
tallyDirectivessequence
- Apply directives until ties are broken or all directives exhausted
- Recursive separation: As each directive separates participants, smaller groups are formed and process repeats
Head-to-Head Logic
When 2 participants are tied:
- Direct comparison of their match result
- Winner places higher
- If they didn't play: move to next directive
When 3+ participants are tied:
- Calculate statistics among only the tied participants
- Look for clear separation
- If circular tie (A>B>C>A): move to next directive
Head-to-head is applied BEFORE tallyDirectives unless explicitly disabled via headToHead: { disabled: true }.
Implementation Details
Calculated Attributes
For each participant, the following attributes are calculated from match results:
Match-level:
matchUpsWon- Total matches wonmatchUpsLost- Total matches lostmatchUpsCancelled- Matches that were cancelled/abandonedmatchUpsPct- Win percentage:matchUpsWon / (matchUpsWon + matchUpsLost)
Set-level:
setsWon- Total sets wonsetsLost- Total sets lostsetsPct- Win percentage:setsWon / (setsWon + setsLost)
Game-level:
gamesWon- Total games wongamesLost- Total games lostgamesPct- Win percentage:gamesWon / (gamesWon + gamesLost)
Point-level:
pointsWon- Total points wonpointsLost- Total points lostpointsPct- Win percentage:pointsWon / (pointsWon + pointsLost)
Incomplete matches:
defaults- Number of times participant defaultedwalkovers- Number of times participant walked overretirements- Number of times participant retiredallDefaults- Total of all exit statuses (defaults + walkovers + retirements)
TEAM events (tieMatchUps):
tieMatchUpsWon- Individual matches wontieMatchUpsLost- Individual matches losttieSinglesWon- Singles matches wontieSinglesLost- Singles matches losttieDoublesWon- Doubles matches wontieDoublesLost- Doubles matches lost
Final result:
groupOrder- Final placement (1-based, lower is better)GEMscore- Composite score (if configured)
Calculation Scoping
Global scope (idsFilter: false):
// Calculate from ALL group matches
matchUpsPct = matchUpsWon / (matchUpsWon + matchUpsLost);
Filtered scope (idsFilter: true):
// Calculate from ONLY matches among tied participants
// Example: If A, B, C are tied, only count A-B, A-C, B-C matches
matchUpsPct = matchUpsWon_amongTied / (matchUpsWon_amongTied + matchUpsLost_amongTied);
Group Totals
Participant-based (groupTotals: false - default):
gamesPct = gamesWon / (gamesWon + gamesLost);
// Each participant's denominator is their own games
// Sum of all gamesPct can exceed 1.0
Group-based (groupTotals: true):
totalGroupGames = sum of all gamesWon by all participants
gamesPct = gamesWon / totalGroupGames
// Sum of all gamesPct = 1.0
Directive Processing
Directives are applied sequentially:
for (const directive of tallyDirectives) {
// 1. Check maxParticipants threshold
if (directive.maxParticipants && tiedCount > directive.maxParticipants) {
continue; // Skip this directive
}
// 2. Calculate attribute (with idsFilter if specified)
const values = calculateAttribute(directive, tiedParticipants);
// 3. Sort (reversed if specified)
const sorted = directive.reversed
? sortAscending(values) // Least to greatest
: sortDescending(values); // Greatest to least
// 4. Separate into groups by value
const groups = groupByValue(sorted);
// 5. Recursively process each group
groups.forEach((group) => {
if (group.length > 1) {
// Still tied, continue with next directive
} else {
// Resolved! Assign groupOrder
}
});
}
Precision in Percentages
Percentages are calculated with configurable precision:
const precision = Math.pow(10, tallyPolicy?.precision || 3);
// precision: 3 → 1000 → 0.667
// precision: 5 → 100000 → 0.66667
matchUpsPct = Math.round((matchUpsWon / matchUpsTotal) * precision) / precision;
Policy Structure
type RoundRobinTallyPolicy = {
policyName?: string;
// Initial grouping attribute
groupOrderKey?:
| 'matchUpsWon'
| 'tieMatchUpsWon'
| 'tieSinglesWon'
| 'tieDoublesWon'
| 'pointsWon'
| 'gamesWon'
| 'setsWon'
| 'gamesPct'
| 'setsPct'
| 'pointsPct'
| 'matchUpsPct';
// Tiebreaking sequence
tallyDirectives?: Array<{
attribute: string; // Which statistic to compare
idsFilter?: boolean; // Compare only tied participants
reversed?: boolean; // Least-to-greatest instead of greatest-to-least
maxParticipants?: number; // Only apply if # tied <= threshold
groupTotals?: boolean; // Use group totals for percentages
disableHeadToHead?: boolean; // Skip head-to-head for this directive
}>;
// Head-to-head configuration
headToHead?: {
disabled?: boolean; // Skip head-to-head entirely
};
// Percentage calculation options
groupTotalGamesPlayed?: boolean; // Use all group games for gamesPct
groupTotalSetsPlayed?: boolean; // Use all group sets for setsPct
precision?: number; // Decimal precision (default: 3)
// Disqualification rules
disqualifyDefaults?: boolean; // Push defaulting participants to bottom
disqualifyWalkovers?: boolean; // Push walkover participants to bottom
// Exclude specific match statuses
excludeMatchUpStatuses?: string[]; // e.g., ['ABANDONED', 'INCOMPLETE']
// Credit for incomplete matches
setsCreditForDefaults?: boolean;
setsCreditForWalkovers?: boolean;
setsCreditForRetirements?: boolean;
gamesCreditForDefaults?: boolean;
gamesCreditForWalkovers?: boolean;
gamesCreditForRetirements?: boolean;
gamesCreditForTiebreakSets?: boolean; // Default: true
// Composite score attributes
GEMscore?: string[]; // Attributes to include in GEMscore calculation
};
Built-in Policies
Default Tally Policy
Standard tiebreaking procedure:
import { POLICY_ROUND_ROBIN_TALLY_DEFAULT } from 'tods-competition-factory';
const policy = POLICY_ROUND_ROBIN_TALLY_DEFAULT[POLICY_TYPE_ROUND_ROBIN_TALLY];
// {
// policyName: 'Default Round Robin Tally',
// groupOrderKey: 'matchUpsWon',
// tallyDirectives: [
// { attribute: 'matchUpsPct', idsFilter: false },
// { attribute: 'allDefaults', reversed: true, idsFilter: false },
// { attribute: 'defaults', reversed: true, idsFilter: false },
// { attribute: 'walkovers', reversed: true, idsFilter: false },
// { attribute: 'retirements', reversed: true, idsFilter: false },
// { attribute: 'setsPct', idsFilter: false },
// { attribute: 'gamesPct', idsFilter: false },
// { attribute: 'pointsPct', idsFilter: false },
// { attribute: 'matchUpsPct', idsFilter: true },
// { attribute: 'setsPct', idsFilter: true },
// { attribute: 'gamesPct', idsFilter: true },
// { attribute: 'pointsPct', idsFilter: true },
// ],
// }
Tiebreaking sequence:
- Match win percentage (all group matches)
- Fewest all defaults (any exit status)
- Fewest defaults
- Fewest walkovers
- Fewest retirements
- Set win percentage (all group matches)
- Game win percentage (all group matches)
- Point win percentage (all group matches)
- Match win percentage (among tied teams only)
- Set win percentage (among tied teams only)
- Game win percentage (among tied teams only)
- Point win percentage (among tied teams only)
JTT Tally Policy
Junior Team Tennis format:
import { POLICY_ROUND_ROBIN_TALLY_JTT } from 'tods-competition-factory';
// {
// policyName: 'JTT Round Robin Tally',
// groupOrderKey: 'gamesWon',
// tallyDirectives: [
// { attribute: 'matchUpsPct', idsFilter: true },
// { attribute: 'gamesWon', idsFilter: false },
// { attribute: 'matchUpsWon', idsFilter: false },
// { attribute: 'tieMatchUpsWon', idsFilter: false },
// { attribute: 'setsWon', idsFilter: false },
// ],
// }
Key features:
- Primary criterion: Most games won
- Head-to-head first: matchUpsPct with idsFilter: true
- Designed for team tennis where games won is primary metric
TOC Tally Policy
Tournament of Champions format:
import { POLICY_ROUND_ROBIN_TALLY_TOC } from 'tods-competition-factory';
// {
// policyName: 'TOC Round Robin Tally',
// groupOrderKey: 'matchUpsPct',
// tallyDirectives: [
// { attribute: 'matchUpsPct', idsFilter: true, maxParticipants: 2 },
// { attribute: 'gamesPct', idsFilter: false },
// { attribute: 'gamesWon', idsFilter: false },
// { attribute: 'gamesLost', idsFilter: false, reversed: true },
// ],
// }
Key features:
- maxParticipants: 2 on head-to-head (only if 2 teams tied)
- Games percentage for 3+ team ties
- Fewest games lost as final tiebreaker
Key Attributes
groupOrderKey
Purpose: Initial attribute to group participants by before applying tiebreakers.
Valid values:
'matchUpsWon'(default)'tieMatchUpsWon'(for TEAM events)'tieSinglesWon'(for TEAM events)'tieDoublesWon'(for TEAM events)'pointsWon''gamesWon''setsWon''gamesPct''setsPct''pointsPct''matchUpsPct'
Example:
const policy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
groupOrderKey: 'gamesWon', // Group by total games won
tallyDirectives: [{ attribute: 'gamesPct', idsFilter: false }],
},
};
tallyDirectives
Purpose: Sequence of tiebreaking rules applied in order.
Structure:
{
attribute: string; // Statistic to compare
idsFilter?: boolean; // Scope to tied participants only
reversed?: boolean; // Reverse sort order (least-to-greatest)
maxParticipants?: number; // Threshold-based application
groupTotals?: boolean; // Use group totals for percentages
}
Directive attributes:
matchUpsPct- Match win percentagesetsPct- Set win percentagegamesPct- Game win percentagepointsPct- Point win percentagematchUpsWon- Total matches wonsetsWon- Total sets wongamesWon- Total games wonpointsWon- Total points wonmatchUpsLost- Total matches lostsetsLost- Total sets lostgamesLost- Total games lostpointsLost- Total points lostdefaults- Number of defaultswalkovers- Number of walkoversretirements- Number of retirementsallDefaults- All exit statuses combinedtieMatchUpsWon- Individual matches won (TEAM events)tieSinglesWon- Singles matches won (TEAM events)tieDoublesWon- Doubles matches won (TEAM events)
See also: maxParticipants documentation for threshold-based tiebreaking
idsFilter
Purpose: Scope calculations to only the tied participants.
When false (default):
- Statistics calculated from ALL group matches
- Example: gamesPct = gamesWon / (gamesWon + gamesLost) across entire group
When true:
- Statistics calculated ONLY from matches among tied participants
- Example: If A, B, C are tied, only count A-B, A-C, B-C matches
- Used for head-to-head comparisons
Example:
tallyDirectives: [
// Stage 1: Overall group performance
{ attribute: 'gamesPct', idsFilter: false },
// Stage 2: Performance among tied teams only
{ attribute: 'gamesPct', idsFilter: true },
];
reversed
Purpose: Reverse sort order from greatest-to-least to least-to-greatest.
Use cases:
- Fewest losses
- Fewest defaults/walkovers/retirements
- Fewest penalty minutes
- Any "lower is better" metric
Example:
tallyDirectives: [
{ attribute: 'gamesWon', reversed: false }, // Most games won
{ attribute: 'gamesLost', reversed: true }, // Fewest games lost
];
maxParticipants
Purpose: Only apply directive when number of tied participants does not exceed threshold.
Common usage:
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2 // Only if exactly 2 teams tied
}
Why this exists:
- 2 teams: Direct head-to-head works (one beat the other)
- 3+ teams: Circular ties possible (A>B>C>A)
- Sports rules universally distinguish 2-team vs 3+-team ties
See: maxParticipants documentation for complete details and sports precedents
groupTotals
Purpose: Use total group games/sets for percentage calculations instead of participant totals.
When false (default):
gamesPct = gamesWon / (gamesWon + gamesLost)
// Sum of all gamesPct can be > 1.0
When true:
gamesPct = gamesWon / (total games played by all participants)
// Sum of all gamesPct = 1.0
Example:
tallyDirectives: [
{
attribute: 'gamesPct',
idsFilter: false,
groupTotals: true, // Use total group games as denominator
},
];
Or set at top level:
{
groupTotalGamesPlayed: true, // All gamesPct calculations
groupTotalSetsPlayed: true, // All setsPct calculations
}
headToHead
Purpose: Configure head-to-head tiebreaking behavior.
Default behavior:
- When 2 participants tied: Compare their head-to-head record
- When 3+ participants tied: Try to separate with head-to-head among all tied
Disable completely:
{
headToHead: { disabled: true },
tallyDirectives: [
// Head-to-head skipped, go straight to directives
{ attribute: 'gamesPct', idsFilter: false },
],
}
Note: Head-to-head happens BEFORE tallyDirectives. Disabling it forces all tiebreaking through directives.
Percentage Calculations
precision
Purpose: Control decimal precision for percentage calculations.
Default: 3 (1000)
Example:
{
precision: 5, // Results: 0.66667 instead of 0.667
}
Formula: Math.pow(10, precision)
- precision: 3 → 1000 → 0.667
- precision: 5 → 100000 → 0.66667
- precision: 7 → 10000000 → 0.6666667
groupTotalGamesPlayed / groupTotalSetsPlayed
Purpose: Use total group games/sets as denominator for percentage calculations.
Example scenario:
4 participants in round robin (6 total matches):
- Participant A: 12 games won, 6 games lost
- Participant B: 10 games won, 8 games lost
- Participant C: 8 games won, 10 games lost
- Participant D: 6 games won, 12 games lost
Total: 36 games won, 36 games lost (72 games total)
Without groupTotalGamesPlayed:
A: 12/18 = 66.7%
B: 10/18 = 55.6%
C: 8/18 = 44.4%
D: 6/18 = 33.3%
Total: 200%
With groupTotalGamesPlayed:
A: 12/72 = 16.7%
B: 10/72 = 13.9%
C: 8/72 = 11.1%
D: 6/72 = 8.3%
Total: 50% (games won / total games)
Credit for Incomplete Matches
setsCreditForDefaults / setsCreditForWalkovers / setsCreditForRetirements
Purpose: Award winner full sets when opponent doesn't complete match.
Default: All false
Example:
// Best of 3 (2 sets to win)
// Match: A vs B, score 6-3, B retires
// Without setsCreditForRetirements:
A: 1 set won
// With setsCreditForRetirements:
A: 2 sets won (awarded setsToWin)
Policy:
{
setsCreditForRetirements: true,
setsCreditForDefaults: true,
setsCreditForWalkovers: true,
}
gamesCreditForDefaults / gamesCreditForWalkovers / gamesCreditForRetirements
Purpose: Award winner estimated games when opponent doesn't complete match.
Default: All false
Example:
// Best of 3, standard scoring (6 games per set)
// Match: A vs B, score 6-3, B retires
// Without gamesCreditForRetirements:
A: 6 games won
// With gamesCreditForRetirements:
A: 12 games won (estimated from setsToWin * 6)
gamesCreditForTiebreakSets
Purpose: Count tiebreak set as 1 game won.
Default: true
Example:
// Match: A vs B, score 6-3 3-6 [10-5]
// With gamesCreditForTiebreakSets: true
A: 6 + 3 + 1 = 10 games won
B: 3 + 6 = 9 games won
// With gamesCreditForTiebreakSets: false
A: 6 + 3 = 9 games won
B: 3 + 6 = 9 games won
Policy:
{
gamesCreditForTiebreakSets: false, // Don't count [10-x] as game
}
Disqualification
disqualifyDefaults / disqualifyWalkovers
Purpose: Push participants who defaulted/walked over to bottom of group order.
Default: true (in most built-in policies)
Example:
// 4 participants:
// A: 2-1
// B: 2-1
// C: 1-2
// D: 1-2 (with 1 DEFAULT)
// With disqualifyDefaults: true
Order: A, B, C, D (4th - pushed to bottom)
// With disqualifyDefaults: false
Order: A, B, C/D tied (use tiebreakers)
Policy:
{
disqualifyDefaults: true,
disqualifyWalkovers: true,
}
Exclude Match Statuses
excludeMatchUpStatuses
Purpose: Exclude specific match statuses from tally calculations.
Use cases:
- Exclude ABANDONED matches
- Exclude INCOMPLETE matches
- Exclude matches that shouldn't count toward standings
Example:
{
excludeMatchUpStatuses: ['ABANDONED', 'INCOMPLETE'],
}
Effect:
- Excluded matches don't count toward wins/losses
- Not included in percentage calculations
- Treated as if they never happened
GEMscore
Purpose: Create composite score from multiple attributes.
Formula: Weighted combination of percentage attributes
Attributes:
matchUpsPcttieMatchUpsPctsetsPctgamesPctpointsPct
Result:
participantResults: {
participantId123: {
matchUpsWon: 2,
matchUpsLost: 1,
matchUpsPct: 0.667,
setsPct: 0.714,
gamesPct: 0.650,
GEMscore: 85234567, // Composite integer score
}
}
Usage:
- Alternative ranking metric
- Visual representation of overall performance
- Can be used as tiebreaker attribute
Usage Examples
Example 1: Standard Tournament
import { POLICY_TYPE_ROUND_ROBIN_TALLY, POLICY_ROUND_ROBIN_TALLY_DEFAULT } from 'tods-competition-factory';
const tournament = {
policyDefinitions: POLICY_ROUND_ROBIN_TALLY_DEFAULT,
};
tournamentEngine.generateDrawDefinition({
drawType: ROUND_ROBIN,
policyDefinitions: tournament.policyDefinitions,
drawSize: 16,
// ... other options
});
Example 2: Custom Tiebreaking
const customPolicy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
policyName: 'Custom Tournament 2024',
groupOrderKey: 'matchUpsWon',
tallyDirectives: [
// 1. Head-to-head if exactly 2 tied
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2,
},
// 2. Games percentage (all matches)
{
attribute: 'gamesPct',
idsFilter: false,
},
// 3. Sets percentage (all matches)
{
attribute: 'setsPct',
idsFilter: false,
},
// 4. Games won among tied teams
{
attribute: 'gamesWon',
idsFilter: true,
},
// 5. Fewest games lost
{
attribute: 'gamesLost',
idsFilter: false,
reversed: true,
},
],
disqualifyDefaults: true,
disqualifyWalkovers: true,
},
};
Example 3: Team Tennis (TEAM Events)
const teamPolicy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
policyName: 'Team Championship',
groupOrderKey: 'tieMatchUpsWon', // Individual matches won
tallyDirectives: [
// Team match record among tied teams
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2,
},
// Individual matches won
{
attribute: 'tieMatchUpsWon',
idsFilter: false,
},
// Singles matches won
{
attribute: 'tieSinglesWon',
idsFilter: false,
},
// Doubles matches won
{
attribute: 'tieDoublesWon',
idsFilter: false,
},
// Sets won
{
attribute: 'setsWon',
idsFilter: false,
},
],
},
};
Example 4: Games-Based Ranking
const gamesPolicy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
policyName: 'Games Primary',
groupOrderKey: 'gamesWon',
tallyDirectives: [
// Most games won
{
attribute: 'gamesWon',
idsFilter: false,
},
// Fewest games lost
{
attribute: 'gamesLost',
idsFilter: false,
reversed: true,
},
// Game percentage
{
attribute: 'gamesPct',
idsFilter: false,
},
// Head-to-head if 2 teams
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2,
},
],
// Award games for walkovers/defaults
gamesCreditForWalkovers: true,
gamesCreditForDefaults: true,
},
};
Example 5: Hockey-Style with Penalty Minutes
const hockeyPolicy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
policyName: 'Hockey Tournament',
groupOrderKey: 'matchUpsWon',
tallyDirectives: [
// Head-to-head (2 teams only)
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2,
},
// Most wins
{
attribute: 'matchUpsWon',
idsFilter: false,
},
// Goal average (custom attribute - would need implementation)
// { attribute: 'goalAverage', idsFilter: false },
// Fewest penalty minutes (custom attribute)
// { attribute: 'penaltyMinutes', idsFilter: false, reversed: true },
// Fallback to games
{
attribute: 'gamesPct',
idsFilter: false,
},
],
},
};
Best Practices
1. Always Provide Fallback Directives
// ✓ GOOD: Multiple tiebreakers
tallyDirectives: [
{ attribute: 'matchUpsPct', idsFilter: true, maxParticipants: 2 },
{ attribute: 'gamesPct', idsFilter: false },
{ attribute: 'gamesWon', idsFilter: false },
{ attribute: 'gamesLost', idsFilter: false, reversed: true },
];
// ✗ BAD: Single tiebreaker might not resolve all ties
tallyDirectives: [{ attribute: 'matchUpsPct', idsFilter: true, maxParticipants: 2 }];
2. Use maxParticipants for Head-to-Head
// ✓ GOOD: Only apply head-to-head when it makes sense
{
attribute: 'matchUpsPct',
idsFilter: true,
maxParticipants: 2 // Skip if 3+ teams (circular ties possible)
}
// ✗ RISKY: Head-to-head with 3+ teams can be indeterminate
{
attribute: 'matchUpsPct',
idsFilter: true // May not separate circular ties
}
3. Order Directives Logically
// ✓ GOOD: Broad criteria first, narrow criteria later
tallyDirectives: [
{ attribute: 'matchUpsPct', idsFilter: false }, // Overall record
{ attribute: 'setsPct', idsFilter: false }, // Overall sets
{ attribute: 'matchUpsPct', idsFilter: true }, // Head-to-head
{ attribute: 'setsPct', idsFilter: true }, // H2H sets
];
// ✗ CONFUSING: Narrow then broad
tallyDirectives: [
{ attribute: 'matchUpsPct', idsFilter: true }, // H2H first
{ attribute: 'matchUpsPct', idsFilter: false }, // Then overall
];
4. Document Tournament-Specific Rules
const tournamentPolicy = {
[POLICY_TYPE_ROUND_ROBIN_TALLY]: {
policyName: 'Spring Championship 2024',
// Document: "Per tournament regulations section 5.3"
groupOrderKey: 'matchUpsWon',
tallyDirectives: [
// Rule 5.3.1: Head-to-head if 2 teams tied
{ attribute: 'matchUpsPct', idsFilter: true, maxParticipants: 2 },
// Rule 5.3.2: Games won percentage
{ attribute: 'gamesPct', idsFilter: false },
// Rule 5.3.3: Fewest games lost
{ attribute: 'gamesLost', idsFilter: false, reversed: true },
],
},
};
Related Documentation
- maxParticipants - Threshold-based tiebreaking rules
- Seeding Policy - How participants are seeded into draws
- Feed-In Policy - Consolation feed patterns
- Policies Overview - All policy types
Methods
tallyParticipantResults
Calculate participant results and group order based on the round robin tally policy.
import { tallyParticipantResults } from 'tods-competition-factory';
const { participantResults, order, report, readableReport } = tallyParticipantResults({
matchUps, // Array of matchUps
policyDefinitions, // Policy definitions object
matchUpFormat, // Optional default format
generateReport: true, // Optional: generate detailed tiebreaking report
});
// participantResults: {
// [participantId]: {
// matchUpsWon: number;
// matchUpsLost: number;
// setsWon: number;
// setsLost: number;
// gamesWon: number;
// gamesLost: number;
// pointsWon: number;
// pointsLost: number;
// matchUpsPct: number;
// setsPct: number;
// gamesPct: number;
// pointsPct: number;
// defaults: number;
// walkovers: number;
// retirements: number;
// allDefaults: number;
// groupOrder: number; // Final placement (when bracket complete)
// provisionalOrder: number; // Current placement (when incomplete)
// GEMscore?: number; // If configured
// }
// }
When generateReport: true, the function returns detailed information showing exactly how tiebreaks were resolved step-by-step. This is invaluable for:
- Explaining to participants why they finished in a specific position
- Verifying that tiebreaking followed the expected policy
- Debugging complex tiebreaking scenarios
- Creating transparent tournament reports
See Query Governor - tallyParticipantResults for complete documentation of the generateReport parameter and examples of using the tiebreaking report.
Notes
groupOrder vs provisionalOrder
- groupOrder: Assigned when bracket is complete
- provisionalOrder: Assigned when bracket is incomplete
- See
provisionalPositioningin Draws Governor
Bracket Completion
Group order is ONLY assigned when all required matchUps are complete:
const bracketComplete = relevantMatchUps.filter((matchUp) => isComplete(matchUp)).length === relevantMatchUps.length;
if (bracketComplete && groupOrder) {
// Assign groupOrder to participantResults
}
For incomplete brackets, use provisionalOrder instead.
Circular Ties
With 3+ participants, circular ties are possible:
- Team A beats Team B
- Team B beats Team C
- Team C beats Team A
- All teams 1-1 head-to-head
Solution: Use maxParticipants: 2 on head-to-head rules to skip circular tie scenarios.
See maxParticipants documentation for details.
Testing
Comprehensive test coverage in src/tests/documentation/roundRobinTallyPolicy.test.ts:
- ✅ All built-in policies
- ✅ Precision calculations
- ✅ maxParticipants thresholds
- ✅ Reversed attributes
- ✅ Disqualification rules
- ✅ Exclude match statuses
- ✅ Credit for incomplete matches
- ✅ groupTotals calculations
- ✅ idsFilter scoping
- ✅ Head-to-head configurations
- ✅ GEMscore generation
- ✅ TEAM event statistics
- ✅ Circular tie scenarios
Total: 27 tests, all passing ✅