Skip to main content

Competitive Bands Policy

The Competitive Bands Policy (POLICY_TYPE_COMPETITIVE_BANDS) defines thresholds for categorizing match competitiveness based on score spreads. This enables statistical analysis of match competitiveness and helps identify close vs. one-sided matches.

Policy Type: competitiveBands

When to Use:

  • Analyzing match competitiveness patterns
  • Generating competitive profile reports
  • Identifying dominant vs. competitive performances
  • Statistical analysis of tournament quality
  • Evaluating player performance under pressure

Policy Structure

{
competitiveBands: {
policyName?: string; // Optional policy identifier
profileBands: {
DECISIVE: number; // Threshold for decisive matches (%)
ROUTINE: number; // Threshold for routine matches (%)
// Matches above ROUTINE threshold are considered COMPETITIVE
};
predictionModel?: { // Optional — used by predictMatchUpCompetitiveBands
competitiveAnchors: Array<{ delta: number; probability: number }>;
decisiveAnchors: Array<{ delta: number; probability: number }>;
};
}
}

Score Spread Categories:

  • DECISIVE: One-sided matches with score spreads ≤ DECISIVE threshold
  • ROUTINE: Normal competitive matches with spreads ≤ ROUTINE threshold
  • COMPETITIVE: Very close matches with spreads > ROUTINE threshold

Default Policy

The factory provides POLICY_COMPETITIVE_BANDS_DEFAULT:

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

// Default thresholds:
// {
// competitiveBands: {
// policyName: 'Competitive Bands Default',
// profileBands: {
// DECISIVE: 20, // Score spread ≤ 20% = decisive win
// ROUTINE: 50 // Score spread ≤ 50% = routine match
// }
// }
// }

Example Score Classifications:

// Set score: 6-0 (opponent won 0 of 6 games = 0%)
// Spread: 0% → DECISIVE

// Set score: 6-1 (opponent won 1 of 7 games = 14%)
// Spread: 14% → DECISIVE

// Set score: 6-2 (opponent won 2 of 8 games = 25%)
// Spread: 25% → ROUTINE

// Set score: 6-4 (opponent won 4 of 10 games = 40%)
// Spread: 40% → ROUTINE

// Set score: 7-5 (opponent won 5 of 12 games = 42%)
// Spread: 42% → ROUTINE

// Set score: 7-6 (opponent won 6 of 13 games = 46%)
// Spread: 46% → ROUTINE

// Set score: 7-6(8) (tiebreak 10-8, total games won: 6.5/13.5 = 48%)
// Spread: 48% → ROUTINE

// Match that goes to 3rd set tiebreak
// Spread: >50% → COMPETITIVE

Basic Examples

Attach Default Policy

import { tournamentEngine } from 'tods-competition-factory';
import { POLICY_COMPETITIVE_BANDS_DEFAULT } from 'tods-competition-factory';

tournamentEngine.setState(tournamentRecord);

// Attach default competitive bands
const result = tournamentEngine.attachPolicies({
policyDefinitions: POLICY_COMPETITIVE_BANDS_DEFAULT,
});

Custom Competitive Bands

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

// Stricter definition of "decisive"
const strictBands = {
[POLICY_TYPE_COMPETITIVE_BANDS]: {
policyName: 'Strict Competitive Bands',
profileBands: {
DECISIVE: 10, // Only bagels/breadsticks are decisive
ROUTINE: 40, // Tighter definition of routine
},
},
};

tournamentEngine.attachPolicies({
policyDefinitions: strictBands,
});

Looser Competitive Bands

// More lenient definition (fewer "competitive" matches)
const looseBands = {
[POLICY_TYPE_COMPETITIVE_BANDS]: {
policyName: 'Loose Competitive Bands',
profileBands: {
DECISIVE: 30, // More matches classified as decisive
ROUTINE: 60, // Fewer matches classified as competitive
},
},
};

Using Competitive Bands

Get Match Competitive Profile

// Get competitive profile for a single match
const { competitiveProfile } = tournamentEngine.getMatchUpCompetitiveProfile({
matchUpId: 'match-1',
});

console.log(competitiveProfile);
// 'DECISIVE' | 'ROUTINE' | 'COMPETITIVE'

Get Tournament Statistics

// Analyze all matches in tournament
const { competitiveBands } = tournamentEngine.getMatchUpsStats();

console.log(competitiveBands);
// {
// DECISIVE: { count: 15, pct: 25 },
// ROUTINE: { count: 35, pct: 58 },
// COMPETITIVE: { count: 10, pct: 17 }
// }

Get Participant Statistics

// Get competitive profile for specific participant
const { participantStats } = tournamentEngine.getParticipantStats({
participantId: 'player-1',
withCompetitiveProfiles: true,
});

console.log(participantStats.competitiveness);
// {
// decisive: { won: 5, lost: 1, played: 6 },
// routine: { won: 3, lost: 2, played: 5 },
// competitive: { won: 2, lost: 1, played: 3 }
// }

console.log(participantStats.decisiveRatio); // 0.429 (6 of 14 matches)
console.log(participantStats.routineRatio); // 0.357
console.log(participantStats.competitiveRatio); // 0.214

Bulk Enrichment Without Full Context Hydration

For analytics that only need competitiveness bucketing (donut charts, summary stats, downloadable reports), allTournamentMatchUps, allEventMatchUps, and allDrawMatchUps accept contextProfile: { withCompetitiveness: true } together with inContext: false. This attaches competitiveProfile to each completed matchUp without paying the per-matchUp cost of addMatchUpContext hydration (participant resolution, exit profiles, round naming, schedule joins, etc.).

const { matchUps } = tournamentEngine.allTournamentMatchUps({
contextProfile: { withCompetitiveness: true },
inContext: false,
});

// Each completed matchUp now has matchUp.competitiveProfile attached:
// { competitiveness: 'DECISIVE' | 'ROUTINE' | 'COMPETITIVE', spread: number, ... }

The same flag works on allEventMatchUps({ eventId, ... }) and allDrawMatchUps({ drawId, ... }).

Behavior

  • The competitive-bands policy is resolved with the standard three-tier lookup: explicit policyDefinitions argument → getAppliedPolicies (event → draw → tournament scope) → POLICY_COMPETITIVE_BANDS_DEFAULT fixture.
  • Each matchUp is shallow-copied before competitiveProfile is attached, so the underlying drawDefinition.matchUps is never mutated.
  • MatchUps with no winningSide (incomplete, BYEs, walkovers without a recorded winner) are returned untouched.
  • MatchUps that already carry competitiveProfile (from an earlier inContext: true pass) are passed through as-is.

When to use this vs. inContext: true

NeedRecommended call
Competitive-band counts only (e.g., overview donut, league-wide histogram)inContext: false + contextProfile: { withCompetitiveness: true }
Per-participant competitive stats (getParticipantStats, getMatchUpsStats)inContext: false + contextProfile: { withCompetitiveness: true } is enough
Anything that joins on participants, schedule, exit profiles, or roundsinContext: true (full hydration; competitiveProfile is attached automatically)

Real-World Use Cases

Tournament Quality Analysis

// Analyze competitiveness of tournament
const { matchUpsStats } = tournamentEngine.getMatchUpsStats();

const { DECISIVE, ROUTINE, COMPETITIVE } = matchUpsStats.competitiveBands;

console.log(`Tournament Competitiveness:`);
console.log(` Decisive matches: ${DECISIVE.count} (${DECISIVE.pct}%)`);
console.log(` Routine matches: ${ROUTINE.count} (${ROUTINE.pct}%)`);
console.log(` Competitive matches: ${COMPETITIVE.count} (${COMPETITIVE.pct}%)`);

if (COMPETITIVE.pct > 30) {
console.log('High-quality, competitive tournament!');
} else if (DECISIVE.pct > 40) {
console.log('Many one-sided matches - consider better seeding');
}

Seeding Effectiveness

// Analyze if top seeds are dominating (as expected)
const topSeeds = [1, 2, 3, 4];

for (const seedNumber of topSeeds) {
const participant = getParticipantBySeed(seedNumber);

const stats = tournamentEngine.getParticipantStats({
participantId: participant.participantId,
withCompetitiveProfiles: true,
});

const decisivePct = stats.decisiveRatio * 100;

console.log(`Seed ${seedNumber}: ${decisivePct.toFixed(1)}% decisive wins`);

if (decisivePct < 30) {
console.warn(`Seed ${seedNumber} not dominating - possible upset risk`);
}
}

Event-Specific Competitive Bands

Different event types may warrant different thresholds:

// Professional event (expect more competitive matches)
const proBands = {
[POLICY_TYPE_COMPETITIVE_BANDS]: {
policyName: 'Professional Competitive Bands',
profileBands: {
DECISIVE: 15, // Fewer decisive matches expected
ROUTINE: 45, // Lower threshold for "competitive"
},
},
};

tournamentEngine.attachPolicies({
policyDefinitions: proBands,
eventId: 'pro-event-id',
});

// Junior event (expect more lopsided matches)
const juniorBands = {
[POLICY_TYPE_COMPETITIVE_BANDS]: {
policyName: 'Junior Competitive Bands',
profileBands: {
DECISIVE: 25, // More decisive matches expected
ROUTINE: 55, // Higher threshold
},
},
};

tournamentEngine.attachPolicies({
policyDefinitions: juniorBands,
eventId: 'junior-event-id',
});

Predictive Use — predictionModel

The profileBands block describes retrospective competitiveness — how completed matches are classified by score spread. The optional predictionModel block describes predictive competitiveness — how likely a projected matchUp is to land in each band, given the rating delta of the two sides.

The default model is anchored on Dave Fish's 2011 "Need For a Rating System" observations:

  • ~70% competitive at delta ≈ 0 (well-matched / ATP-Slam-equivalent depth)
  • ~55% competitive at delta ≈ 0.5 (WTA-Slam / ITA-Women's depth)
  • ~25% competitive at delta ≈ 1.5 (USTA sectional age-group baseline)

A two-anchor logistic curve is fit through competitiveAnchors for the COMPETITIVE band, and through decisiveAnchors for the DECISIVE band. ROUTINE is the residual.

// Default prediction model
{
competitiveAnchors: [
{ delta: 0, probability: 0.70 },
{ delta: 1.5, probability: 0.25 },
],
decisiveAnchors: [
{ delta: 0, probability: 0.10 },
{ delta: 1.5, probability: 0.55 },
],
}

Example — predict a single matchUp

const result = tournamentEngine.predictMatchUpCompetitiveBands({
side1Rating: 4.5,
side2Rating: 5.0,
});
// → { competitive: 0.55, decisive: 0.18, routine: 0.27, delta: 0.5 }

Example — predict an entire draw

const result = tournamentEngine.predictDrawCompetitiveBands({
ratings: [5.5, 5.4, 5.3, 5.2, 5.1, 5.0, 4.9, 4.8],
drawType: 'SINGLE_ELIMINATION',
});
// → {
// competitive: 0.62,
// decisive: 0.13,
// routine: 0.25,
// projectionMode: 'BALANCED_BRACKET',
// projectedPairs: [[5.5, 4.8], [5.4, 4.9], [5.3, 5.0], [5.2, 5.1]],
// expectedMatchCount: 4,
// }

Projection modes

The draw-level predictor projects the matchUps that will play. Three modes are available, auto-resolved from drawType:

ModeAuto-mapped fromPairing
BALANCED_BRACKETSINGLE_ELIMINATION, DOUBLE_ELIMINATION, COMPASS, LUCKY_DRAW, ADAPTIVE, FIRST_MATCH_LOSER_CONSOLATION, FIRST_ROUND_LOSER_CONSOLATION, FEED_IN_CHAMPIONSHIP_TO_QF, FEED_IN_CHAMPIONSHIP_TO_SFi-th highest rating vs i-th lowest — mirrors R1 of standard seed placement
ROUND_ROBINROUND_ROBIN, DOUBLE_ROUND_ROBIN, ROUND_ROBIN_WITH_PLAYOFFAll pairs within each group; uses groupSize when supplied
MIN_DELTASWISSAdjacent ratings — minimum delta. Suitable for Swiss R1 and DrawMatic-style rating-balanced pairing

projectionMode can also be passed explicitly, overriding the auto-mapping. This is the path for ad-hoc draw types (e.g., DrawMatic) that aren't first-class draw-type constants.

Custom anchors per audience

// Junior tournament — wider rating spreads tolerated
const juniorPolicy = {
[POLICY_TYPE_COMPETITIVE_BANDS]: {
policyName: 'Junior Competitive Bands',
profileBands: { DECISIVE: 25, ROUTINE: 55 },
predictionModel: {
competitiveAnchors: [
{ delta: 0, probability: 0.65 },
{ delta: 2.0, probability: 0.3 },
],
decisiveAnchors: [
{ delta: 0, probability: 0.15 },
{ delta: 2.0, probability: 0.5 },
],
},
},
};

Scope and limits

  • Singles only. Doubles ratings (paired/team aggregation) are not currently modeled.
  • Bracket projection is R1-only for elimination draws and full pairwise for round-robin. Later rounds depend on results that have not happened; full-tournament Monte Carlo simulation is out of scope.
  • The model is statistical, not deterministic. Output is a probability distribution suitable for ranking candidate plans against one another, not for predicting a specific matchUp's outcome.

Notes

  • Default thresholds (20%, 50%) are based on typical tennis match distributions
  • Walkover matches are excluded from competitive analysis
  • Retired matches are classified based on completed score
  • Tiebreaks are included in spread calculations (fractional games)
  • Thresholds are percentages (0-100 scale)
  • Policy affects analytics only - does not impact match progression
  • Used by getMatchUpCompetitiveProfile, getMatchUpsStats, getParticipantStats
  • Can be attached at tournament, event, or draw level
  • More decisive matches (lower spread) suggest better seeding or skill gaps