Competition Governor
import { competitionGovernor } from 'tods-competition-factory';
The competitionGovernor provides functions for managing multi-tournament competitions where several tournamentRecords are held in shared state. These methods enable linking tournaments together, sharing venues and schedules across tournaments, and managing competition-wide extensions.
Use Cases:
- Multi-site tournament management (e.g., US Open across multiple locations)
- Tournament series with shared participants and venues
- Linked qualifying and main draw tournaments
- Federation-level competition management
linkTournaments
Links all tournaments currently loaded in competitionEngine state by adding a LINKED_TOURNAMENTS extension to each tournament record. Linked tournaments can share venues, schedule cross-tournament matchUps, and be managed as a unified competition. See examples: Linked Tournaments & Shared Venues.
Purpose: Establish relationships between multiple tournaments to enable competition-wide operations like shared venue management, cross-tournament scheduling, and unified participant tracking.
When to Use:
- Managing multi-site tournaments (same event across venues)
- Linking qualifying and main draw tournaments
- Creating tournament series with shared resources
- Enabling cross-tournament scheduling and venue management
- Federating multiple tournaments under single administration
Parameters:
{
tournamentRecords?: TournamentRecords; // Optional - from engine state if not provided
}
Returns:
{
success: boolean;
error?: ErrorType; // MISSING_TOURNAMENT_RECORDS if no tournaments in state
}
Link Mechanism:
- Adds LINKED_TOURNAMENTS extension to each tournament
- Extension contains array of all linked tournament IDs
- Each tournament knows about all other linked tournaments
- Minimum of 2 tournaments required for linking
Examples:
import { competitionEngine } from 'tods-competition-factory';
// Load multiple tournaments into competition state
await competitionEngine.setState([qualifyingTournament, mainDrawTournament, doublesOnlyTournament]);
// Link all tournaments in state
const result = await competitionEngine.linkTournaments();
console.log(result.success); // true
// Verify links were created
const { linkedTournamentIds } = await competitionEngine.getLinkedTournamentIds();
console.log(linkedTournamentIds);
// {
// 'qualifying-id': ['main-draw-id', 'doubles-only-id'],
// 'main-draw-id': ['qualifying-id', 'doubles-only-id'],
// 'doubles-only-id': ['qualifying-id', 'main-draw-id']
// }
// Check extension on individual tournament
const { tournamentRecord } = competitionEngine.getTournament({
tournamentId: 'qualifying-id',
});
const linkedExtension = tournamentRecord.extensions.find((ext) => ext.name === 'linkedTournaments');
console.log(linkedExtension.value);
// { tournamentIds: ['qualifying-id', 'main-draw-id', 'doubles-only-id'] }
// Add another tournament and re-link
competitionEngine.setTournamentRecord(teamEventTournament);
await competitionEngine.linkTournaments();
// All four tournaments now linked together
const { linkedTournamentIds } = await competitionEngine.getLinkedTournamentIds();
console.log(Object.keys(linkedTournamentIds).length); // 4
// Single tournament in state - success but no links created
competitionEngine.reset();
competitionEngine.setState(singleTournament);
const result = await competitionEngine.linkTournaments();
console.log(result.success); // true (but no extension added)
Notes:
- Requires at least 2 tournaments loaded in state
- Overwrites any existing LINKED_TOURNAMENTS extension
- Links are bidirectional - each tournament links to all others
- Safe to call multiple times (idempotent with current state)
- New tournament added after initial linking requires relinking
- Enables cross-tournament venue sharing and scheduling
- Required for
allCompetitionMatchUps()to work across tournaments - Does not copy venues between tournaments automatically
- Tournament must have at least tournamentId property
- Returns success with single tournament (no-op)
unlinkTournament
Unlinks a specific tournament from other tournaments loaded in state by removing it from LINKED_TOURNAMENTS extensions across all linked tournaments.
Purpose: Remove a tournament from a linked competition while preserving links between remaining tournaments. Useful for removing qualifying tournaments after completion or isolating a tournament for independent management.
When to Use:
- Removing completed qualifying tournament from active competition
- Isolating a tournament for independent scheduling
- Handling tournament withdrawal from series
- Breaking up competition into separate managements
- Removing test tournaments from production data
Parameters:
{
tournamentId: string; // Required - ID of tournament to unlink
tournamentRecords?: TournamentRecords; // Optional - from engine state if not provided
}
Returns:
{
success: boolean;
error?: ErrorType; // MISSING_TOURNAMENT_ID, INVALID_VALUES, etc.
}
Unlinking Logic:
- Removes tournamentId from LINKED_TOURNAMENTS extensions in other tournaments
- Removes LINKED_TOURNAMENTS extension from unlinked tournament
- If remaining linked tournaments = 1, removes their extension too (no point linking to self)
- Preserves links between remaining tournaments (if 2+)
Examples:
import { competitionEngine } from 'tods-competition-factory';
// Setup: Link three tournaments
await competitionEngine.setState([qualifyingTournament, mainDrawTournament, doublesOnlyTournament]);
await competitionEngine.linkTournaments();
// Unlink the qualifying tournament (now complete)
const result = await competitionEngine.unlinkTournament({
tournamentId: 'qualifying-id',
});
console.log(result.success); // true
// Check remaining links
const { linkedTournamentIds } = await competitionEngine.getLinkedTournamentIds();
console.log(linkedTournamentIds);
// {
// 'main-draw-id': ['doubles-only-id'],
// 'doubles-only-id': ['main-draw-id']
// }
// Note: qualifying-id no longer appears
// Verify qualifying tournament has no links
const { tournamentRecord } = competitionEngine.getTournament({
tournamentId: 'qualifying-id',
});
const linkedExtension = tournamentRecord.extensions?.find((ext) => ext.name === 'linkedTournaments');
console.log(linkedExtension); // undefined
// Unlink down to single tournament - removes extension entirely
await competitionEngine.unlinkTournament({
tournamentId: 'doubles-only-id',
});
const { tournamentRecord: mainRecord } = competitionEngine.getTournament({
tournamentId: 'main-draw-id',
});
const stillLinked = mainRecord.extensions?.find((ext) => ext.name === 'linkedTournaments');
console.log(stillLinked); // undefined (can't be linked to only yourself)
// Error handling
result = await competitionEngine.unlinkTournament({
tournamentId: 'nonexistent-id',
});
console.log(result.error); // MISSING_TOURNAMENT_ID
// Unlinking already unlinked tournament succeeds
result = await competitionEngine.unlinkTournament({
tournamentId: 'qualifying-id',
});
console.log(result.success); // true (idempotent)
// Multi-tournament workflow example
// Day 1-3: Qualifying
await competitionEngine.setState([qualifyingTournament]);
// ... run qualifying ...
// Day 4-10: Main draw starts, link with qualifying
await competitionEngine.setState([qualifyingTournament, mainDrawTournament]);
await competitionEngine.linkTournaments();
// ... access combined data ...
// Day 11+: Qualifying done, unlink it
await competitionEngine.unlinkTournament({
tournamentId: qualifyingTournament.tournamentId,
});
// ... continue with just main draw ...
Notes:
- Tournament must exist in state
- Modifies LINKED_TOURNAMENTS extension across all affected tournaments
- When 2 tournaments remain after unlinking, they stay linked to each other
- When 1 tournament remains after unlinking, its extension is removed (can't self-link)
- Unlinked tournament's extension is always removed
- Does not remove tournament from state - only removes links
- Safe to call on already unlinked tournaments (idempotent)
- Does not affect venues, participants, or schedule
- Useful for phased competition management (qualifying → main → finals)
- Does not automatically update
allCompetitionMatchUps()results
unlinkTournaments
Removes LINKED_TOURNAMENTS extension from all tournaments currently loaded in state. Effectively dissolves the competition into independent tournaments.
Purpose: Break all links between tournaments in a competition, returning each tournament to independent management. Useful for competition teardown or converting linked competitions back to standalone tournaments.
When to Use:
- Ending a competition series
- Resetting tournament relationships for fresh linking
- Converting linked competition back to independent tournaments
- Cleaning up test data
- Preparing tournaments for export as standalone records
Parameters:
{
tournamentRecords?: TournamentRecords; // Optional - from engine state if not provided
discover?: boolean; // Traverse extensions to find nested links
}
Returns:
{
success: boolean;
error?: ErrorType; // MISSING_TOURNAMENT_RECORDS if no tournaments
}
Examples:
import { competitionEngine } from 'tods-competition-factory';
// Setup: Linked competition
await competitionEngine.setState([
qualifyingTournament,
mainDrawTournament,
doublesOnlyTournament,
teamEventTournament,
]);
await competitionEngine.linkTournaments();
// Verify links exist
let { linkedTournamentIds } = await competitionEngine.getLinkedTournamentIds();
console.log(Object.keys(linkedTournamentIds).length); // 4 tournaments linked
// Unlink all tournaments
const result = await competitionEngine.unlinkTournaments();
console.log(result.success); // true
// Verify all links removed
({ linkedTournamentIds } = await competitionEngine.getLinkedTournamentIds());
console.log(linkedTournamentIds); // {} (empty object)
// Check individual tournament
const { tournamentRecord } = competitionEngine.getTournament({
tournamentId: 'main-draw-id',
});
const linkedExtension = tournamentRecord.extensions?.find((ext) => ext.name === 'linkedTournaments');
console.log(linkedExtension); // undefined
// Competition teardown workflow
// 1. Competition complete
// 2. Unlink all tournaments
await competitionEngine.unlinkTournaments();
// 3. Export individual tournaments
const tournaments = await competitionEngine.getState();
for (const [tournamentId, tournamentRecord] of Object.entries(tournaments)) {
await exportTournament(tournamentRecord); // Independent exports
}
// Fresh start workflow
// Reset links before establishing new relationships
await competitionEngine.unlinkTournaments();
// ... modify tournament composition ...
await competitionEngine.linkTournaments(); // Create fresh links
// Idempotent - safe to call multiple times
await competitionEngine.unlinkTournaments();
const result2 = await competitionEngine.unlinkTournaments();
console.log(result2.success); // true
Notes:
- Removes LINKED_TOURNAMENTS extension from all tournaments in state
- Does not remove tournaments from state - only removes links
- Equivalent to calling
unlinkTournament()for each tournament - More efficient than unlinking individually
- Idempotent - safe to call when no links exist
- Does not affect venues, participants, schedules, or other extensions
- Required before creating new link structure with different tournaments
- Use
removeExtension({ name: 'linkedTournaments' })for same effect — see removeExtension in tournament governor
getTournamentIds
Returns an array of all tournament IDs currently loaded in the competition engine state.
const { tournamentIds } = competitionEngine.getTournamentIds();
console.log(tournamentIds); // ['tournament-1-id', 'tournament-2-id']
Returns:
{
tournamentIds: string[];
success: boolean;
}
Use Cases:
- Iterate over all tournaments in competition
- Verify tournaments are loaded
- Check competition size
getLinkedTournamentIds
Returns a mapping object where each tournament ID maps to an array of other tournament IDs it is linked to via the LINKED_TOURNAMENTS extension.
const { linkedTournamentIds } = competitionEngine.getLinkedTournamentIds();
console.log(linkedTournamentIds);
// {
// 'qualifying-id': ['main-draw-id', 'doubles-id'],
// 'main-draw-id': ['qualifying-id', 'doubles-id'],
// 'doubles-id': ['qualifying-id', 'main-draw-id']
// }
// Check if specific tournament is linked
const qualifyingLinks = linkedTournamentIds['qualifying-id'];
if (qualifyingLinks?.includes('main-draw-id')) {
console.log('Qualifying is linked to main draw');
}
Returns:
{
linkedTournamentIds: {
[tournamentId: string]: string[]; // Array of linked tournament IDs
};
error?: ErrorType; // MISSING_TOURNAMENT_RECORDS if no tournaments in state
}
Notes:
- Each tournament ID maps to an array of OTHER tournament IDs (excludes itself)
- Empty array means tournament has no links
- Only returns tournaments that have LINKED_TOURNAMENTS extension
- Use after
linkTournaments()to verify links were created
Competition Policy Methods
The following methods manage per-draw competition state for policy-driven rating systems (dynamic form ratings, pressure ratings, leaderboards). Competition state is stored as a COMPETITION_STATE extension on a drawDefinition and is governed by a POLICY_TYPE_COMPETITION applied policy.
initializeCompetitionState
Initializes competition state for a draw by computing baseline ratings for all participants and storing the initial state as a COMPETITION_STATE extension on the draw definition.
Parameters:
{
tournamentRecord: Tournament; // Required - tournament containing participants
drawDefinition: DrawDefinition; // Required - draw to attach state to
participantIds: string[]; // Required - participants to include in competition state
event?: Event; // Optional - used for eventType-aware rating resolution
}
Returns:
{
success?: boolean;
competitionState?: CompetitionState; // The initialized state (participantStates + roundStates)
error?: ErrorType; // MISSING_DRAW_DEFINITION, MISSING_VALUE
}
Example:
const result = engine.initializeCompetitionState({
tournamentRecord,
drawDefinition,
participantIds: ['p1', 'p2', 'p3', 'p4'],
event,
});
const { competitionState } = result;
console.log(competitionState.participantStates['p1']);
// {
// participantId: 'p1',
// baselineRating: 1500,
// dynamicFormRating: 1500,
// pressureRating: 0,
// roundsPlayed: 0,
// wins: 0, losses: 0, draws: 0,
// totalPointsWon: 0, totalPointsLost: 0,
// ratingHistory: []
// }
Notes:
- Requires a
POLICY_TYPE_COMPETITIONpolicy to be applied; returns success with no state if policy is absent - Baseline ratings are resolved via the policy's
ratingPolicy.baselineRating.scaleNameusing the same infrastructure as DrawMatic - For DOUBLES events, individual participant ratings are aggregated using the policy's
ratingAggregationmethod (AVERAGE, MIN, MAX, SUM) - State is persisted as a
COMPETITION_STATEextension on the drawDefinition
processCompetitionMatchUp
Processes a single completed matchUp, updating both participants' dynamic form ratings, pressure ratings, win/loss records, and rating histories based on the competition policy.
Parameters:
{
tournamentRecord?: Tournament; // Optional - for policy resolution
drawDefinition: DrawDefinition; // Required - draw containing competition state
matchUp: MatchUp; // Required - the completed matchUp to process
event?: Event; // Optional - for policy resolution
}
Returns:
{
success?: boolean;
error?: ErrorType; // MISSING_DRAW_DEFINITION, MISSING_MATCHUP
}
Example:
const result = engine.processCompetitionMatchUp({
tournamentRecord,
drawDefinition,
matchUp: completedMatchUp,
event,
});
Notes:
- Both sides must have participantIds and existing participant states; silently succeeds if not
- Uses Elo-style expected score calculations with the policy's
logisticScaleandkFactor - Dynamic form rating updates use dynamic-vs-dynamic expectations
- Pressure rating deltas use actual-vs-baseline expectations
- Point counts are derived from the matchUp score via
deriveCountables - Updated state is persisted back to the
COMPETITION_STATEextension
processCompetitionRound
Processes all completed matchUps in a given round number, updating competition state for each, and marks the round as processed to prevent double-processing.
Parameters:
{
tournamentRecord?: Tournament; // Optional - for policy resolution
drawDefinition: DrawDefinition; // Required - draw containing competition state
roundNumber: number; // Required - the round to process
matchUps: MatchUp[]; // Required - all matchUps in the draw (filtered internally)
event?: Event; // Optional - for policy resolution
}
Returns:
{
success?: boolean;
error?: ErrorType; // MISSING_DRAW_DEFINITION
}
Example:
const result = engine.processCompetitionRound({
tournamentRecord,
drawDefinition,
roundNumber: 1,
matchUps: allDrawMatchUps,
event,
});
Notes:
- Filters matchUps to the specified
roundNumberand only those with awinningSideor a completed matchUpStatus - Delegates to
processCompetitionMatchUpfor each qualifying matchUp - Marks the round as
processed: trueincompetitionState.roundStatesto prevent re-processing - Idempotent: silently succeeds if the round was already processed
resetCompetitionState
Removes the COMPETITION_STATE extension from a draw definition, clearing all accumulated competition data.
Parameters:
{
drawDefinition: DrawDefinition; // Required - draw to reset
}
Returns:
{
success?: boolean;
error?: ErrorType; // MISSING_DRAW_DEFINITION
}
Example:
const result = engine.resetCompetitionState({ drawDefinition });
// Competition state is now cleared; call initializeCompetitionState to start fresh
Notes:
- Sets the
COMPETITION_STATEextension value toundefined - Does not remove the competition policy; only clears accumulated state
- Useful before re-initializing state after draw modifications
getCompetitionState
Retrieves the current CompetitionState stored on a draw definition, including all participant states and round processing records.
Parameters:
{
drawDefinition: DrawDefinition; // Required - draw to read state from
}
Returns:
{
competitionState?: CompetitionState;
// CompetitionState contains:
// participantStates: Record<string, CompetitionParticipantState>
// roundStates: Record<number, { roundNumber: number; processed: boolean }>
}
Example:
const { competitionState } = engine.getCompetitionState({ drawDefinition });
if (competitionState) {
const participantIds = Object.keys(competitionState.participantStates);
console.log(`Tracking ${participantIds.length} participants`);
const processedRounds = Object.values(competitionState.roundStates)
.filter((r) => r.processed)
.map((r) => r.roundNumber);
console.log(`Processed rounds: ${processedRounds}`);
}
Notes:
- Returns
undefinedforcompetitionStateif no state has been initialized - Read-only; does not modify the draw definition
getCompetitionPolicy
Retrieves the POLICY_TYPE_COMPETITION policy applied to a draw, event, or tournament. The competition policy governs rating calculations, victory conditions, and leaderboard sorting.
Parameters:
{
tournamentRecord?: Tournament; // Optional - checked for applied policies
drawDefinition?: DrawDefinition; // Optional - checked for applied policies
event?: Event; // Optional - checked for applied policies
}
Returns:
{
competitionPolicy?: CompetitionPolicy;
// CompetitionPolicy contains ratingPolicy, victoryPolicy, etc.
}
Example:
const { competitionPolicy } = engine.getCompetitionPolicy({
tournamentRecord,
drawDefinition,
event,
});
if (competitionPolicy) {
console.log(competitionPolicy.ratingPolicy.dynamicFormRating.kFactor);
console.log(competitionPolicy.victoryPolicy.primaryRanking);
}
Notes:
- Uses the standard applied policies resolution chain (draw -> event -> tournament)
- Returns
undefinedforcompetitionPolicyif no competition policy is applied
getCompetitionLeaderboard
Returns a sorted leaderboard of all participants in the competition, ranked according to the policy's primaryRanking criterion and tiebreak rules.
Parameters:
{
tournamentRecord?: Tournament; // Optional - for policy resolution
drawDefinition: DrawDefinition; // Required - draw containing competition state
event?: Event; // Optional - for policy resolution
}
Returns:
{
leaderboard?: CompetitionLeaderboardRow[];
// Each row contains:
// participantId: string
// rank: number
// baselineRating: number
// dynamicFormRating: number
// pressureRating: number
// wins: number
// losses: number
// draws: number
// pointsWon: number
// pointsLost: number
}
Example:
const { leaderboard } = engine.getCompetitionLeaderboard({
tournamentRecord,
drawDefinition,
event,
});
for (const row of leaderboard) {
console.log(`#${row.rank} ${row.participantId}: ${row.wins}W-${row.losses}L (form: ${row.dynamicFormRating})`);
}
Notes:
- Primary ranking options:
PRESSURE_RATING,DYNAMIC_FORM_RATING,WINS,POINTS - Tiebreak methods (applied in policy-defined order):
POINT_DIFFERENTIAL,DYNAMIC_FORM_RATING,PRESSURE_RATING,HEAD_TO_HEAD,HEAD_TO_HEAD_PRESSURE,STRENGTH_OF_OPPOSITION - Returns an empty array if competition state or policy is not present
getCompetitionParticipantState
Retrieves the competition state for a single participant, including ratings, win/loss record, and rating history.
Parameters:
{
drawDefinition: DrawDefinition; // Required - draw containing competition state
participantId: string; // Required - participant to look up
}
Returns:
{
participantState?: CompetitionParticipantState;
// CompetitionParticipantState contains:
// participantId: string
// baselineRating: number
// dynamicFormRating: number
// pressureRating: number
// roundsPlayed: number
// wins: number
// losses: number
// draws: number
// totalPointsWon: number
// totalPointsLost: number
// ratingHistory: RatingHistoryEntry[]
}
Example:
const { participantState } = engine.getCompetitionParticipantState({
drawDefinition,
participantId: 'p1',
});
if (participantState) {
console.log(`Rating: ${participantState.dynamicFormRating}`);
console.log(`Record: ${participantState.wins}-${participantState.losses}`);
console.log(`Pressure: ${participantState.pressureRating}`);
console.log(`Matches: ${participantState.ratingHistory.length}`);
}
Notes:
- Returns
undefinedforparticipantStateif competition state is not initialized or participant is not found - Rating history entries include per-matchUp details: opponent, rating before/after, pressure delta, actual vs expected output