CP-SAT - Sports Scheduling
Problem: build a double round-robin schedule where teams play balanced home and away games with as few breaks as possible.
Show code
import { CpSat, setWorkerBridgeEnabled, type SatParameters } from 'or-tools-wasm/cp-sat';
type CpModelInput = {
name: string;
variables: Array<{ name: string; domain: number[] }>;
constraints: Array<
| { name?: string; linear: { vars: number[]; coeffs: number[]; domain: number[] } }
| { name?: string; table: { vars: number[]; values: number[] } }
>;
objective?: { vars: number[]; coeffs: number[]; offset?: number; scaling_factor?: number };
};
function buildSportsScheduleModel(numTeams: number): CpModelInput {
const numDays = 2 * numTeams - 2;
const matchesPerHalf = numTeams - 1;
const variables: CpModelInput['variables'] = [];
const constraints: CpModelInput['constraints'] = [];
const addVar = (name: string) => {
const index = variables.length;
variables.push({ name, domain: [0, 1] });
return index;
};
const addSum = (name: string, vars: number[], domain: number[]) => {
constraints.push({
name,
linear: { vars, coeffs: Array(vars.length).fill(1), domain },
});
};
const fixture: number[][][] = [];
for (let day = 0; day < numDays; ++day) {
fixture[day] = [];
for (let home = 0; home < numTeams; ++home) {
fixture[day][home] = [];
for (let away = 0; away < numTeams; ++away) {
fixture[day][home][away] = home === away ? -1 : addVar(`d${day}_h${home}_a${away}`);
}
}
}
const atHome = Array.from({ length: numDays }, (_, day) =>
Array.from({ length: numTeams }, (_, team) => addVar(`home_d${day}_t${team}`)),
);
for (let day = 0; day < numDays; ++day) {
for (let team = 0; team < numTeams; ++team) {
const plays: number[] = [];
const homes: number[] = [];
for (let other = 0; other < numTeams; ++other) {
if (team === other) continue;
plays.push(fixture[day][team][other], fixture[day][other][team]);
homes.push(fixture[day][team][other]);
}
addSum(`plays_once_d${day}_t${team}`, plays, [1, 1]);
constraints.push({
name: `link_home_d${day}_t${team}`,
linear: { vars: [...homes, atHome[day][team]], coeffs: [...homes.map(() => 1), -1], domain: [0, 0] },
});
}
}
for (let team = 0; team < numTeams; ++team) {
for (let other = team + 1; other < numTeams; ++other) {
const firstHalf: number[] = [];
const secondHalf: number[] = [];
for (let day = 0; day < matchesPerHalf; ++day) {
firstHalf.push(fixture[day][team][other], fixture[day][other][team]);
const secondDay = day + matchesPerHalf;
secondHalf.push(fixture[secondDay][team][other], fixture[secondDay][other][team]);
}
addSum(`first_half_${team}_${other}`, firstHalf, [1, 1]);
addSum(`second_half_${team}_${other}`, secondHalf, [1, 1]);
}
}
const breaks: number[] = [];
for (let team = 0; team < numTeams; ++team) {
for (let day = 0; day < numDays - 1; ++day) {
const breakVar = addVar(`break_t${team}_d${day}`);
breaks.push(breakVar);
constraints.push({
table: {
vars: [atHome[day][team], atHome[day + 1][team], breakVar],
values: [0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1],
},
});
}
}
return {
name: 'sports_schedule',
variables,
constraints,
objective: { vars: breaks, coeffs: Array(breaks.length).fill(1), offset: 0, scaling_factor: 1 },
};
}
setWorkerBridgeEnabled(true);
const numTeams = 4;
const model = await CpSat.createModel(buildSportsScheduleModel(numTeams));
const validation = await CpSat.validate(model);
if (!validation.ok) throw new Error(validation.message);
const params: SatParameters = { numWorkers: 4, logSearchProgress: true };
const result = await CpSat.solve(model, params);
console.log(result.response?.objectiveValue, result.response?.solution);
- Each team plays exactly one match per day.
- Every pair of teams plays twice: once home and once away.
- The season is split into two halves, and each pair meets once per half.
- A break is two home games in a row or two away games in a row for the same team.
- The solver minimizes total breaks while satisfying the round-robin constraints.
Provide parameters and run the solver to see a schedule.
Solution
Run the solver to view the solution.
Status: