Conflict Reporting
Overview
Schedule conflict reporting identifies players who have overlapping match commitments that violate recovery time requirements or scheduling policies. The Competition Factory automatically detects and reports these conflicts to help tournament directors maintain player welfare and schedule integrity.
How Conflict Detection Works
Conflicts are detected when a player's matches are scheduled too close together, based on:
- Scheduling Policy: Uses format-specific average match times and recovery periods
- Actual Match End Times: If available, uses real completion time plus recovery
- Custom Thresholds: Optionally override with specific minute differences
Conflict Calculation
A conflict occurs when:
Match 1 projected end time + recovery time > Match 2 scheduled start time
Where:
- Projected end time =
scheduledTime + averageMinutesOR actualendTime - Recovery time = Policy-defined rest period (format and category specific)
- Match 2 start time = Next scheduled match for the player
Retrieving Conflict Information
Basic Conflict Detection
import { tournamentEngine } from 'tods-competition-factory';
const {
participants,
participantIdsWithConflicts, // Array of participantIds with conflicts
} = tournamentEngine.getParticipants({
withMatchUps: true,
scheduleAnalysis: true,
});
// Check for conflicts
if (participantIdsWithConflicts.length > 0) {
console.log(`${participantIdsWithConflicts.length} players have scheduling conflicts`);
// Get detailed conflict information
const conflictedPlayers = participants.filter((p) => participantIdsWithConflicts.includes(p.participantId));
conflictedPlayers.forEach((player) => {
console.log(`${player.participantName}:`);
player.scheduleConflicts.forEach((conflict) => {
console.log(` Match ${conflict.matchUpId1} conflicts with ${conflict.matchUpId2}`);
});
});
}
Custom Conflict Threshold
Override policy defaults with a specific minute difference:
const { participants, participantIdsWithConflicts } = tournamentEngine.getParticipants({
scheduleAnalysis: {
scheduledMinutesDifference: 60, // Flag any matches within 60 minutes
},
withMatchUps: true,
});
This approach:
- Ignores policy-defined recovery times
- Flags any matches scheduled less than 60 minutes apart
- Useful for quick validation or stricter requirements
Conflict Data Structure
Participants with conflicts include a scheduleConflicts array:
type Participant = {
participantId: string;
participantName: string;
scheduleConflicts?: Array<{
matchUpId1: string;
matchUpId2: string;
gap?: number; // Minutes between matches (if calculable)
required?: number; // Required minutes based on policy
}>;
matchUps?: MatchUp[]; // Player's scheduled matches
potentialMatchUps?: MatchUp[]; // Future possible matches
};
Conflict Types
1. Confirmed Conflicts - Both matches have scheduled times:
{
matchUpId1: 'match-123', // First match
matchUpId2: 'match-456', // Second match
gap: 45, // Actual minutes between matches
required: 60 // Required recovery time
}
2. Potential Conflicts - Involves future/conditional matches:
{
matchUpId1: 'match-123', // Current match
matchUpId2: 'match-789', // Potential future match (if player wins)
gap: 30,
required: 60
}
Understanding Potential Matches
potentialMatchUps are matches a player will participate in based on winning or losing their current match. The conflict detector considers these to prevent scheduling issues in later rounds.
Example scenario:
- Player has Semifinals match at 2:00 PM
- Finals scheduled at 4:00 PM (if player wins)
- Recovery time required: 90 minutes
- Conflict detected: Not enough time between potential end (3:30 PM + recovery) and finals start (4:00 PM)
Resolution Strategies
1. Identify All Conflicts
const { participants, participantIdsWithConflicts } = tournamentEngine.getParticipants({
scheduleAnalysis: true,
withMatchUps: true,
withPotentialMatchUps: true,
});
// Group conflicts by severity
const criticalConflicts = [];
const minorConflicts = [];
participants
.filter((p) => p.scheduleConflicts?.length > 0)
.forEach((player) => {
player.scheduleConflicts.forEach((conflict) => {
const shortfall = (conflict.required || 0) - (conflict.gap || 0);
if (shortfall > 30) {
criticalConflicts.push({ player, conflict, shortfall });
} else {
minorConflicts.push({ player, conflict, shortfall });
}
});
});
console.log(`Critical conflicts: ${criticalConflicts.length}`);
console.log(`Minor conflicts: ${minorConflicts.length}`);
2. Reschedule Conflicting Matches
// Reschedule one of the conflicting matches
criticalConflicts.forEach(({ player, conflict }) => {
// Get match details
const match1 = player.matchUps.find((m) => m.matchUpId === conflict.matchUpId1);
const match2 = player.matchUps.find((m) => m.matchUpId === conflict.matchUpId2);
// Determine which match to move (usually the later one)
const matchToReschedule = match2;
// Calculate new time with sufficient recovery
const match1End = addMinutes(match1.schedule.scheduledTime, 90); // avg match time
const newTime = addMinutes(match1End, conflict.required);
// Apply new schedule
tournamentEngine.addMatchUpScheduledTime({
matchUpId: matchToReschedule.matchUpId,
scheduledTime: newTime,
scheduledDate: matchToReschedule.schedule.scheduledDate,
});
});
// Re-check for conflicts
const { participantIdsWithConflicts: remainingConflicts } = tournamentEngine.getParticipants({
scheduleAnalysis: true,
});
console.log(`Remaining conflicts: ${remainingConflicts.length}`);
3. Adjust Scheduling Policy
If conflicts are systematic, adjust the policy. These functions add extensions to the tournament record that are read by scheduling functions:
// Increase recovery times (adds tournament-level extension)
tournamentEngine.modifyMatchUpFormatTiming({
matchUpFormat: 'SET3-S:6/TB7',
recoveryTimes: [
{
categoryNames: [],
minutes: { default: 90 }, // Increased from 60
},
],
});
// Reduce daily limits (adds tournament-level extension)
tournamentEngine.setMatchUpDailyLimits({
dailyLimits: { SINGLES: 1, DOUBLES: 1, total: 2 }, // More restrictive
});
These modifications persist at the tournament level and affect all subsequent scheduling operations until explicitly changed.
4. Manual Override
For unavoidable conflicts (e.g., late-night match followed by early match next day):
// Document the exception
tournamentEngine.addExtension({
matchUpId: 'match-456',
extension: {
name: 'scheduleException',
value: {
reason: 'Tournament director approved - player consent obtained',
approvedBy: 'TD Name',
timestamp: new Date().toISOString(),
},
},
});
Best Practices
Conflict Prevention
Run conflict analysis before finalizing schedules
// Before publishing order of play
const { participantIdsWithConflicts } = tournamentEngine.getParticipants({
scheduleAnalysis: true,
});
if (participantIdsWithConflicts.length > 0) {
console.warn('STOP: Conflicts detected - resolve before publishing');
}Check after each schedule change
- Re-run analysis after rescheduling matches
- Ensure fixes don't create new conflicts
Consider potential matches
- Enable
withPotentialMatchUpsto detect future conflicts - Particularly important for semifinals/finals scheduling
- Enable
Conflict Resolution Priority
- Critical conflicts (gap < required by 60+ minutes) - immediate resolution
- Moderate conflicts (gap < required by 30-60 minutes) - high priority
- Minor conflicts (gap < required by < 30 minutes) - evaluate case-by-case
Communication
When conflicts exist:
- Notify affected players immediately
- Obtain consent if conflict cannot be resolved
- Document exceptions for tournament records
- Explain how the conflict will be managed (e.g., medical timeout extended, warm-up time adjusted)
Common Scenarios
Multi-Event Players
Players in both singles and doubles often face conflicts:
// Find multi-event players with conflicts
const multiEventPlayers = participants.filter((p) => {
const eventTypes = new Set(p.matchUps?.map((m) => m.eventType));
return eventTypes.size > 1 && p.scheduleConflicts?.length > 0;
});
multiEventPlayers.forEach((player) => {
console.log(`${player.participantName} has ${player.scheduleConflicts.length} cross-event conflicts`);
});
Back-to-Back Matches
Players scheduled on multiple courts simultaneously:
// Detect impossible simultaneous scheduling
const simultaneousMatches = participants.filter((p) => {
const matches = p.matchUps || [];
const times = matches.map((m) => m.schedule?.scheduledTime).filter(Boolean);
return new Set(times).size < times.length; // Duplicate times = simultaneous
});
Finals Day Conflicts
Semifinals too close to finals:
// Check specific round scheduling
const finalists = participants.filter((p) => {
const hasSemifinal = p.matchUps?.some((m) => m.roundName === 'Semifinals');
const hasFinal = p.potentialMatchUps?.some((m) => m.roundName === 'Finals');
return hasSemifinal && hasFinal && p.scheduleConflicts?.length > 0;
});
Validation Workflow
Complete validation before publishing schedules:
// Comprehensive schedule validation
function validateSchedule(tournamentEngine) {
const validation = {
conflicts: [],
warnings: [],
errors: [],
};
// 1. Check for scheduling conflicts
const { participantIdsWithConflicts, participants } = tournamentEngine.getParticipants({
scheduleAnalysis: true,
withMatchUps: true,
withPotentialMatchUps: true,
});
if (participantIdsWithConflicts.length > 0) {
validation.errors.push({
type: 'SCHEDULE_CONFLICT',
count: participantIdsWithConflicts.length,
details: participants
.filter((p) => participantIdsWithConflicts.includes(p.participantId))
.map((p) => ({
name: p.participantName,
conflicts: p.scheduleConflicts,
})),
});
}
// 2. Check daily limits
const overLimitPlayers = participants.filter((p) => {
const matchesByDate = groupBy(p.matchUps, (m) => m.schedule?.scheduledDate);
return Object.values(matchesByDate).some((matches) => matches.length > 3);
});
if (overLimitPlayers.length > 0) {
validation.warnings.push({
type: 'DAILY_LIMIT_EXCEEDED',
count: overLimitPlayers.length,
});
}
// 3. Check for unscheduled matches
const { matchUps } = tournamentEngine.allTournamentMatchUps();
const unscheduled = matchUps.filter((m) => !m.schedule?.scheduledTime);
if (unscheduled.length > 0) {
validation.warnings.push({
type: 'UNSCHEDULED_MATCHES',
count: unscheduled.length,
});
}
return validation;
}
// Run validation
const validation = validateSchedule(tournamentEngine);
if (validation.errors.length > 0) {
console.error('Schedule has errors - cannot publish:');
console.error(JSON.stringify(validation.errors, null, 2));
} else if (validation.warnings.length > 0) {
console.warn('Schedule has warnings - review before publishing:');
console.warn(JSON.stringify(validation.warnings, null, 2));
} else {
console.log('Schedule validation passed - ready to publish');
}
Related Documentation
- Scheduling Overview - Understanding scheduling workflows
- Scheduling Policy - Configuring recovery times and daily limits
- Automated Scheduling - How conflicts are prevented during auto-scheduling
- Query Governor - API reference for getParticipants method