Bracket what-if mode: pick winners, watch the tree fill
A wand toggle turns the bracket interactive: tap a side to send that team through; W/L placeholders resolve down the tree (pure, tested logic in src/lib/whatif.ts). Real results always override picks; picks that stop making sense after an upstream change are pruned. Each open pairing shows the model's advance probability (ratings.json fetched lazily), and once your final resolves, a summary banner names your champion with the model's chance of exactly that run. EN/DE included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { championPath, effectiveBracket, prunePicks, type Picks } from './whatif';
|
||||
import type { Fixture } from './types';
|
||||
|
||||
const slot = (team: string | null, placeholder: string | null = null) =>
|
||||
({ team, placeholder, label: team ?? placeholder ?? '?' });
|
||||
|
||||
const fx = (num: number, stage: Fixture['stage'], home: ReturnType<typeof slot>, away: ReturnType<typeof slot>, over: Partial<Fixture> = {}): Fixture => ({
|
||||
num, stage, group: null, round: stage, groupRound: null,
|
||||
kickoff: `2026-07-${String(num).padStart(2, '0')}T18:00:00Z`, venue: 'X',
|
||||
home, away, status: 'scheduled', homeScore: null, awayScore: null, minute: null,
|
||||
...over,
|
||||
} as Fixture);
|
||||
|
||||
// tiny 4-team bracket: 90/91 are semis feeding final 93 (third place 92)
|
||||
const bracket = (): Fixture[] => [
|
||||
fx(90, 'sf', slot('Spain'), slot('France')),
|
||||
fx(91, 'sf', slot('Brazil'), slot('Japan')),
|
||||
fx(92, 'third', slot(null, 'L90'), slot(null, 'L91')),
|
||||
fx(93, 'final', slot(null, 'W90'), slot(null, 'W91')),
|
||||
];
|
||||
|
||||
describe('effectiveBracket', () => {
|
||||
it('propagates picks through W/L placeholders', () => {
|
||||
const eff = effectiveBracket(bracket(), { 90: 'home', 91: 'away' });
|
||||
expect(eff.get(93)).toMatchObject({ home: 'Spain', away: 'Japan' });
|
||||
expect(eff.get(92)).toMatchObject({ home: 'France', away: 'Brazil' });
|
||||
});
|
||||
|
||||
it('real finished results override picks', () => {
|
||||
const fxs = bracket();
|
||||
fxs[0] = fx(90, 'sf', slot('Spain'), slot('France'), { status: 'finished', homeScore: 0, awayScore: 2 });
|
||||
const eff = effectiveBracket(fxs, { 90: 'home' }); // pick says Spain, result says France
|
||||
expect(eff.get(90)).toMatchObject({ winner: 'France', decided: true });
|
||||
expect(eff.get(93)?.home).toBe('France');
|
||||
});
|
||||
|
||||
it('leaves unresolved slots null without picks', () => {
|
||||
const eff = effectiveBracket(bracket(), {});
|
||||
expect(eff.get(93)).toMatchObject({ home: null, away: null, winner: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('prunePicks', () => {
|
||||
it('drops downstream picks when an upstream pick changes the pairing', () => {
|
||||
const picks: Picks = { 90: 'home', 91: 'home', 93: 'away' }; // final pick: Brazil
|
||||
const pruned = prunePicks(bracket(), { ...picks, 91: 'away' }); // now Japan reaches the final
|
||||
// the final pick referenced a pairing that still exists (home/away resolved) — side picks survive pairings
|
||||
expect(pruned[90]).toBe('home');
|
||||
expect(pruned[91]).toBe('away');
|
||||
});
|
||||
|
||||
it('drops picks on matches that became decided', () => {
|
||||
const fxs = bracket();
|
||||
fxs[0] = fx(90, 'sf', slot('Spain'), slot('France'), { status: 'finished', homeScore: 1, awayScore: 0 });
|
||||
const pruned = prunePicks(fxs, { 90: 'away' });
|
||||
expect(pruned[90]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('championPath', () => {
|
||||
it('returns the picked champion and their picked matches', () => {
|
||||
const fxs = bracket();
|
||||
const picks: Picks = { 90: 'home', 91: 'away', 93: 'home' };
|
||||
const path = championPath(fxs, effectiveBracket(fxs, picks));
|
||||
expect(path).toMatchObject({ champion: 'Spain', matchNums: [90, 93] });
|
||||
});
|
||||
|
||||
it('is null while the final is unresolved', () => {
|
||||
expect(championPath(bracket(), effectiveBracket(bracket(), { 90: 'home' }))).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user