// Group qualification scenarios: enumerate every outcome combination of the // remaining group matches (W/D/L with representative one-goal margins) and // rank each hypothetical table. Top-2 is decidable inside a group; third place // runs through the cross-group best-thirds race and is NOT claimed here. import { rankGroup, type Result } from './standings'; import type { Fixture } from './types'; export interface TeamScenario { team: string; /** outcome combinations in which the team finishes in the top 2 */ top2Count: number; totalCombos: number; guaranteed: boolean; alive: boolean; /** top-2 in every combination where the team wins all its remaining matches * (null when the team has nothing left to play) */ safeWithWin: boolean | null; } const OUTCOMES: [number, number][] = [[1, 0], [0, 0], [0, 1]]; /** Scenarios for one group, or null when enumeration isn't meaningful yet * (nothing left to play, or more than 3 group matches still open). */ export function groupScenarios(teams: string[], groupFixtures: Fixture[]): TeamScenario[] | null { const played: Result[] = []; const remaining: { home: string; away: string }[] = []; for (const f of groupFixtures) { if (!f.home.team || !f.away.team) return null; // group slots must be known if (f.status === 'finished' && f.homeScore != null && f.awayScore != null) { played.push({ home: f.home.team, away: f.away.team, homeScore: f.homeScore, awayScore: f.awayScore }); } else { remaining.push({ home: f.home.team, away: f.away.team }); } } if (remaining.length === 0 || remaining.length > 3) return null; const total = 3 ** remaining.length; const top2 = new Map(teams.map((t) => [t, 0])); const winCombos = new Map(teams.map((t) => [t, { total: 0, top2: 0 }])); for (let combo = 0; combo < total; combo++) { const assumed: Result[] = []; const wonBy = new Set(); const lostOrDrew = new Set(); let c = combo; for (const m of remaining) { const [hs, as] = OUTCOMES[c % 3]!; c = Math.floor(c / 3); assumed.push({ home: m.home, away: m.away, homeScore: hs, awayScore: as }); if (hs > as) { wonBy.add(m.home); lostOrDrew.add(m.away); } else if (as > hs) { wonBy.add(m.away); lostOrDrew.add(m.home); } else { lostOrDrew.add(m.home); lostOrDrew.add(m.away); } } const table = rankGroup(teams, [...played, ...assumed]); const topTwo = new Set(table.slice(0, 2).map((r) => r.team)); for (const t of teams) { if (topTwo.has(t)) top2.set(t, top2.get(t)! + 1); // a combo counts toward "with a win" when the team won every remaining match it plays in const plays = remaining.some((m) => m.home === t || m.away === t); if (plays && wonBy.has(t) && !lostOrDrew.has(t)) { const w = winCombos.get(t)!; w.total++; if (topTwo.has(t)) w.top2++; } } } return teams.map((team) => { const n = top2.get(team)!; const w = winCombos.get(team)!; return { team, top2Count: n, totalCombos: total, guaranteed: n === total, alive: n > 0, safeWithWin: w.total === 0 ? null : w.top2 === w.total, }; }); }