CoursesBlogWhy TrioNxInstructorsFAQExplore Courses

How I Built an AI World Cup Simulator to Settle My Family's Arguments - No Coding

๐Ÿ† FIFA World Cup 2026 โ€” Bracket & Winner Predictor

AUTO-REFRESH OFF โš™๏ธ Admin

USA ยท Canada ยท Mexico ยท June 11 โ€“ July 19, 2026 ยท 48 teams. Click teams to advance them, run scenarios, dial upsets. Connect a live feed in โš™๏ธ Admin.

Last updated: โ€” ๐Ÿ”„ Refresh now

0/31

Matches Decided

0

Upsets

โ€”

Predicted Champion

โ€”

Their Title Odds

๐Ÿ—‚๏ธ Bracket๐Ÿ“Š Probability๐ŸŽฏ Best / Worst Case๐Ÿ“ก Live Data

๐Ÿ“‹ Scenario Presets โ€” each fills the entire bracket (real results stay locked)

๐Ÿ“Š ChalkFavorites always win ๐ŸŒช๏ธ ChaosMaximum upsets ๐ŸŽฒ Realistic SimWeighted by probability ๐Ÿด Morocco RunDark horse to final ๐Ÿ‡บ๐Ÿ‡ธ USA CinderellaHost upset run ๐Ÿ”ฅ Argentina vs BrazilDream final ๐Ÿ’ฅ No.1 Seeds FallTop 3 out early ๐ŸŽฏ One Big UpsetSingle shock, rest to form

๐ŸŽฒ Re-Simulate โ†บ Reset to Real Results โšฝ Show xG

๐ŸŒช๏ธ Upset rate0%

Advancing Upset winner Higher win probability Toss-up / xG Live / final result

๐Ÿ“Š Run Monte Carlo (10k) โšก Quick (2k)

๐ŸŒช๏ธ Upset rate0%

Run a simulation to see each team's chance of reaching every round. Eliminated teams show 0%.

Analyze team: ๐ŸŽฏ Analyze

๐Ÿ“ก Auto-refresh & live status โ€” how it works

A browser-only page can't scrape FIFA/ESPN directly (CORS). Three ways to feed it, configured in โš™๏ธ Admin: (1) manual score entry, (2) a sports-data API key, or (3) your own backend proxy. The app uses adaptive polling โ€” it only calls the API when a match is near kickoff or in play, so you don't burn your quota.

recommendedAdaptive polling schedule

No game today โ†’ idle (no calls). Game within 30 min โ†’ every 5 min. Game live โ†’ every 60โ€“90 sec. Just finished โ†’ every 2 min for 10 min, then stop. This keeps you under free-tier caps (football-data.org: 10 req/min; API-Football free: 100 req/day).

noteLive scores often need a paid tier

On many free tiers, scores are delayed rather than real-time; full goal-by-goal usually needs a paid livescores add-on. The adaptive schedule still works โ€” results just arrive with a short lag.

productionBackend proxy

For a public site, run a tiny server (Node / Cloudflare Worker) that holds your key and forwards the feed. Set its URL in Admin โ†’ "Proxy URL".

Status: idle. Open โš™๏ธ Admin to configure a feed.

๐Ÿ“ฐ Latest confirmed results baked in

Canada 1โ€“0 South Africa (FT) ยท Brazil 2โ€“1 Japan (FT) ยท Paraguay stunned Germany 1โ€“1 (4โ€“3 pens). 15 Round-of-32 ties still to play.

โš™๏ธ Admin / Settings

Configure the live data feed and auto-refresh. Settings are saved in your browser (localStorage) and persist across reloads.

โš ๏ธ Security: anything entered here lives in the browser and is visible in DevTools. For a public site, use a Proxy URL (option 3) so your real API key stays on the server.

Data source Manual entry (no API) football-data.org (X-Auth-Token) API-Football (x-apisports-key) My backend proxy URL

API key

Get a free key: football-data.org (10 req/min) or API-Football (100 req/day).

Proxy URL

Should return JSON: array of {home, away, homeScore, awayScore, status}.

Auto-refresh mode

Adaptive Fixed Off

Adaptive = poll fast only when a game is live/near. Best for quota.

Fixed interval (minutes)

Used only in Fixed mode. 5 min is a safe default; 1โ€“2 min during live play.

๐Ÿ’พ Save settings ๐Ÿ”„ Test fetch

๐Ÿ—‘๏ธ Clear saved picks Close

Settings idle.

Built by a football fan (not a developer) to settle family arguments. Planned with Claude & ChatGPT, built and refined with ChatterMate AI by CleverCreator.ai. Team ratings are illustrative estimates, not official rankings; win probabilities are model outputs, not guarantees. Tournament data is demo/sample unless a verified live feed is connected. For fun and scenario-exploration โ€” not a betting guide.

<script> const T = { "South Africa":{r:1480,f:"๐Ÿ‡ฟ๐Ÿ‡ฆ"}, "Canada":{r:1620,f:"๐Ÿ‡จ๐Ÿ‡ฆ"}, "Netherlands":{r:1990,f:"๐Ÿ‡ณ๐Ÿ‡ฑ"}, "Morocco":{r:1810,f:"๐Ÿ‡ฒ๐Ÿ‡ฆ"}, "Germany":{r:1960,f:"๐Ÿ‡ฉ๐Ÿ‡ช"}, "Paraguay":{r:1560,f:"๐Ÿ‡ต๐Ÿ‡พ"}, "France":{r:2080,f:"๐Ÿ‡ซ๐Ÿ‡ท"}, "Sweden":{r:1640,f:"๐Ÿ‡ธ๐Ÿ‡ช"}, "Belgium":{r:1900,f:"๐Ÿ‡ง๐Ÿ‡ช"}, "Senegal":{r:1750,f:"๐Ÿ‡ธ๐Ÿ‡ณ"}, "USA":{r:1700,f:"๐Ÿ‡บ๐Ÿ‡ธ"}, "Bosnia and Herzegovina":{r:1540,f:"๐Ÿ‡ง๐Ÿ‡ฆ"}, "Spain":{r:2050,f:"๐Ÿ‡ช๐Ÿ‡ธ"}, "Austria":{r:1680,f:"๐Ÿ‡ฆ๐Ÿ‡น"}, "Portugal":{r:2010,f:"๐Ÿ‡ต๐Ÿ‡น"}, "Croatia":{r:1880,f:"๐Ÿ‡ญ๐Ÿ‡ท"}, "Brazil":{r:2070,f:"๐Ÿ‡ง๐Ÿ‡ท"}, "Japan":{r:1780,f:"๐Ÿ‡ฏ๐Ÿ‡ต"}, "Cรดte d'Ivoire":{r:1660,f:"๐Ÿ‡จ๐Ÿ‡ฎ"}, "Norway":{r:1720,f:"๐Ÿ‡ณ๐Ÿ‡ด"}, "Mexico":{r:1710,f:"๐Ÿ‡ฒ๐Ÿ‡ฝ"}, "Ecuador":{r:1650,f:"๐Ÿ‡ช๐Ÿ‡จ"}, "England":{r:2030,f:"๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ"}, "DR Congo":{r:1570,f:"๐Ÿ‡จ๐Ÿ‡ฉ"}, "Switzerland":{r:1790,f:"๐Ÿ‡จ๐Ÿ‡ญ"}, "Algeria":{r:1690,f:"๐Ÿ‡ฉ๐Ÿ‡ฟ"}, "Colombia":{r:1920,f:"๐Ÿ‡จ๐Ÿ‡ด"}, "Ghana":{r:1600,f:"๐Ÿ‡ฌ๐Ÿ‡ญ"}, "Australia":{r:1670,f:"๐Ÿ‡ฆ๐Ÿ‡บ"}, "Egypt":{r:1640,f:"๐Ÿ‡ช๐Ÿ‡ฌ"}, "Argentina":{r:2100,f:"๐Ÿ‡ฆ๐Ÿ‡ท"}, "Cabo Verde":{r:1450,f:"๐Ÿ‡จ๐Ÿ‡ป"}, }; const R32 = [ ["South Africa","Canada","2026-06-28T20:00:00",{h:0,a:1,winner:"Canada"}], ["Netherlands","Morocco","2026-07-01T21:00:00"], ["Germany","Paraguay","2026-06-30T16:30:00",{h:1,a:1,winner:"Paraguay",pen:"3-4"}], ["France","Sweden","2026-06-30T17:00:00"], ["Belgium","Senegal","2026-07-01T16:00:00"], ["USA","Bosnia and Herzegovina","2026-07-01T20:00:00"], ["Spain","Austria","2026-07-02T15:00:00"], ["Portugal","Croatia","2026-07-02T19:00:00"], ["Brazil","Japan","2026-06-29T13:00:00",{h:2,a:1,winner:"Brazil"}], ["Cรดte d'Ivoire","Norway","2026-06-30T13:00:00"], ["Mexico","Ecuador","2026-06-30T21:00:00"], ["England","DR Congo","2026-07-01T12:00:00"], ["Switzerland","Algeria","2026-07-02T23:00:00"], ["Colombia","Ghana","2026-07-03T21:30:00"], ["Australia","Egypt","2026-07-03T14:00:00"], ["Argentina","Cabo Verde","2026-07-03T18:00:00"], ]; const roundNames=["Round of 32","Round of 16","Quarter-finals","Semi-finals","Final"]; const reachLabels=["R16","QF","SF","Final","Champion"]; let upsetRate=0, showXG=false; let cfg={source:"manual",key:"",proxy:"",mode:"adaptive",interval:5}; let pollTimer=null, countdownTimer=null, nextPollAt=0;

function fmtDate(iso){try{return new Date(iso).toLocaleString(undefined,{weekday:'short',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return iso;}} function winProb(a,b){if(!a||!b)return .5;return 1/(1+Math.pow(10,(T[b].r-T[a].r)/400));} function isUpset(w,l){return T[w].r<T[l].r;} function xg(a,b){return Math.max(0.3,1.35+(T[a].r-T[b].r)/400*0.9);} function scoreline(a,b){return `${xg(a,b).toFixed(1)} โ€“ ${xg(b,a).toFixed(1)}`;} function adjProb(a,b){const p=winProb(a,b);return p+(0.5-p)*upsetRate;}

let rounds=[]; function buildInitial(){ rounds=[]; rounds[0]=R32.map(m=>({a:m[0],b:m[1],iso:m[2],date:fmtDate(m[2]),result:m[3]||null,winner:m[3]?m[3].winner:null,ft:!!m[3],live:false,liveScore:null})); for(let r=1;r<5;r++){rounds[r]=[];for(let i=0;i<rounds[r-1].length/2;i++)rounds[r].push({a:null,b:null,date:"TBD",winner:null,live:false});} propagate(); } function propagate(){for(let r=1;r<rounds.length;r++){rounds[r].forEach((m,i)=>{const na=rounds[r-1][i*2].winner,nb=rounds[r-1][i*2+1].winner;if(m.a!==na||m.b!==nb){m.a=na;m.b=nb;}if(m.winner&&m.winner!==m.a&&m.winner!==m.b)m.winner=null;});}} function setWinner(r,i,team){rounds[r][i].winner=team;propagate();for(let rr=r+1;rr<rounds.length;rr++)rounds[rr].forEach(m=>{if(m.winner&&m.winner!==m.a&&m.winner!==m.b)m.winner=null;});savePicks();render();}

function render(){ const br=document.getElementById('bracket');br.innerHTML=''; for(let r=0;r<rounds.length;r++){ const col=document.createElement('div');col.className='round'; const t=document.createElement('div');t.className='round-title'; const dec=rounds[r].filter(m=>m.winner).length; t.innerHTML=`${roundNames[r]}<small>${dec}/${rounds[r].length} decided</small>`; col.appendChild(t); rounds[r].forEach((m,i)=>col.appendChild(matchEl(m,r,i))); br.appendChild(col); } const cc=document.createElement('div');cc.className='round'; const ct=document.createElement('div');ct.className='round-title';ct.innerHTML='Champion';cc.appendChild(ct); const champ=rounds[4][0].winner; const cd=document.createElement('div');cd.className='champ'+(champ?'':' empty'); cd.innerHTML=champ?`<div class="ttl">๐Ÿ† Champion</div><div class="cflag">${T[champ].f}</div><div class="cname">${champ}</div>`:`<div class="ttl">Champion</div><div class="cflag" style="opacity:.3">๐Ÿ†</div><div class="cname">TBD</div>`; cc.appendChild(cd);br.appendChild(cc);updateStats(); } function teamRow(team,other,m,r,i,side){ const row=document.createElement('div');row.className='team'; if(!team){row.classList.add('tbd');row.innerHTML=`<span class="flag">ยท</span><span class="name">TBD</span>`;return row;} const p=winProb(team,other); if(m.winner===team){row.classList.add('winner');if(other&&isUpset(team,other))row.classList.add('upset');} let goals=''; if(m.result){const g=side==='a'?m.result.h:m.result.a;goals=`<span class="gs">${g}</span>`;} else if(m.live&&m.liveScore){const g=side==='a'?m.liveScore.h:m.liveScore.a;goals=`<span class="gs" style="color:#fca5a5">${g}</span>`;} else if(showXG&&other){goals=`<span class="xg">${xg(team,other).toFixed(1)}</span>`;} const showProb=(other&&!m.result&&!m.live); row.innerHTML=`<span class="flag">${T[team].f}</span><span class="name">${team}</span><span class="upbadge">UPSET</span>${goals}<span class="prob">${showProb?Math.round(p*100)+'%':((m.result||m.live)?'':'โ€”')}</span><div class="probbar"><span style="width:${showProb?p*100:0}%"></span></div>`; row.onclick=(e)=>{e.stopPropagation();if(other&&!m.ft)setWinner(r,i,team);}; return row; } function matchEl(m,r,i){ const el=document.createElement('div');el.className='match'; if(m.live)el.classList.add('islive'); else if(m.a&&m.b&&!m.result&&Math.abs(winProb(m.a,m.b)-.5)<.08)el.classList.add('tossup'); const d=document.createElement('div');d.className='match-date'; if(m.live)d.innerHTML=`<span class="livedot"></span><span class="livetag">LIVE</span>`; else if(m.ft)d.innerHTML=`<span class="ft">${m.result&&m.result.pen?'FT (pens '+m.result.pen+')':'Full Time'}</span>`; else d.innerHTML=m.date||'TBD'; d.onclick=(e)=>{e.stopPropagation();if(m.a&&m.b)openModal(m,r,i);}; el.appendChild(d);el.appendChild(teamRow(m.a,m.b,m,r,i,'a'));el.appendChild(teamRow(m.b,m.a,m,r,i,'b')); return el; }

function fillBracket(decider){for(let r=0;r<rounds.length;r++){rounds[r].forEach((m,i)=>{if(!m.a||!m.b)return;if(m.ft&&m.winner)return;m.winner=decider(m.a,m.b,r,i);});propagate();}savePicks();render();} function scenChalk(){buildInitial();applySaved();fillBracket((a,b)=>winProb(a,b)>=.5?a:b);flash("๐Ÿ“Š Chalk โ€” every favorite advances.");} function scenChaos(){buildInitial();applySaved();fillBracket((a,b)=>{const p=winProb(a,b);if(Math.abs(p-.5)<.06)return Math.random()<.5?a:b;return p>=.5?b:a;});flash("๐ŸŒช๏ธ Chaos โ€” underdogs everywhere.");} function scenRandom(){buildInitial();applySaved();fillBracket((a,b)=>Math.random()<adjProb(a,b)?a:b);flash(`๐ŸŽฒ Realistic sim @ ${Math.round(upsetRate*100)}% upset rate.`);} function scenDarkHorse(team){buildInitial();applySaved();fillBracket((a,b)=>{if(a===team)return team;if(b===team)return team;return Math.random()<adjProb(a,b)?a:b;});flash(`๐Ÿด ${team} run โ€” ${rounds[4][0].winner===team?'they win it all!':'see how far they get.'}`);} function scenClash(x,y){buildInitial();applySaved();fillBracket((a,b,r)=>{if(r<4){if(a===x||a===y)return a;if(b===x||b===y)return b;}return Math.random()<adjProb(a,b)?a:b;});const f=rounds[4][0];const made=f.a&&f.b&&((f.a===x&&f.b===y)||(f.a===y&&f.b===x));flash(made?`๐Ÿ”ฅ Dream final: ${x} vs ${y}!`:`๐Ÿ”ฅ Pushed ${x} & ${y} as far as possible.`);} function scenSeedFalls(){buildInitial();applySaved();const top=Object.keys(T).sort((a,b)=>T[b].r-T[a].r).slice(0,3);fillBracket((a,b,r)=>{if(r===0){if(top.includes(a))return b;if(top.includes(b))return a;}return Math.random()<adjProb(a,b)?a:b;});flash(`๐Ÿ’ฅ ${top.join(", ")} crash out early.`);} function scenOneUpset(){buildInitial();applySaved();const open=[];rounds[0].forEach((m,i)=>{if(!m.ft)open.push(i);});if(!open.length){flash("All R32 ties already decided.");return;}const idx=open[Math.floor(Math.random()*open.length)];fillBracket((a,b,r,i)=>{if(r===0&&i===idx)return winProb(a,b)>=.5?b:a;return winProb(a,b)>=.5?a:b;});const m=rounds[0][idx];flash(`๐ŸŽฏ One big upset: ${m.winner} shocks the bracket.`);} function resetBracket(){buildInitial();render();flash("โ†บ Reset to latest real results.");} function toggleXG(){showXG=!showXG;document.getElementById('xgToggle').textContent=showXG?'โšฝ Hide xG':'โšฝ Show xG';render();} function onSlider(){upsetRate=+document.getElementById('upsetSlider').value/100;syncSliders('upsetSlider');} function onSlider2(){upsetRate=+document.getElementById('upsetSlider2').value/100;syncSliders('upsetSlider2');} function syncSliders(src){const v=Math.round(upsetRate*100);['upsetSlider','upsetSlider2'].forEach(id=>{if(id!==src)document.getElementById(id).value=v;});document.getElementById('upsetVal').textContent=v+'%';document.getElementById('upsetVal2').textContent=v+'%';}

function openModal(m,r,i){ const a=m.a,b=m.b,pa=winProb(a,b),pb=1-pa; const fc=(n,va,vb)=>({n,va,vb}); const factors=[fc("Overall Strength",T[a].r,T[b].r),fc("Attack",T[a].r*.96+seed(a,1)*40,T[b].r*.96+seed(b,1)*40),fc("Defense",T[a].r*.94+seed(a,2)*40,T[b].r*.94+seed(b,2)*40),fc("Recent Form",T[a].r*.5+seed(a,3)*900,T[b].r*.5+seed(b,3)*900)]; const editable=!m.ft; document.getElementById('modal').innerHTML=` <h3>${roundNames[r]} ยท Matchup</h3><div class="mdate">${m.date||''}</div> <div class="vs-row"> <div class="vs-team"><div class="bigflag">${T[a].f}</div><div class="tn">${a}</div><div class="pct" style="color:${pa>=pb?'#818cf8':'#94a3b8'}">${Math.round(pa*100)}%</div><div class="score">xG ${xg(a,b).toFixed(2)}</div></div> <div class="vs-mid">vs</div> <div class="vs-team"><div class="bigflag">${T[b].f}</div><div class="tn">${b}</div><div class="pct" style="color:${pb>pa?'#fbbf24':'#94a3b8'}">${Math.round(pb*100)}%</div><div class="score">xG ${xg(b,a).toFixed(2)}</div></div> </div> <div style="text-align:center;font-size:12px;color:var(--muted);margin:-4px 0 4px">Projected: <b style="color:var(--gold)">${scoreline(a,b)}</b></div> <div class="probtrack"><div class="a" style="width:${pa*100}%"></div><div class="b" style="width:${pb*100}%"></div></div> <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted)"><span>${a}</span><span>${b}</span></div> ${editable?`<div class="scoreset"><input type="number" id="scH" min="0" value="0"><span style="color:var(--muted);font-weight:700">โ€“</span><input type="number" id="scA" min="0" value="0"><button class="btn-live" onclick="enterScore(${r},${i})">Enter final score</button></div>`:`<div style="text-align:center;margin:10px 0;color:var(--gold);font-weight:700">Final: ${a} ${m.result.h}โ€“${m.result.a} ${b}${m.result.pen?' (pens '+m.result.pen+')':''}</div>`} <div class="factors">${factors.map(f=>{const t=f.va+f.vb,wa=f.va/t*100;return `<div class="factor"><span class="lbl">${f.n}</span><div class="fbar"><span style="width:${wa}%;background:${wa>=50?'#6366f1':'#f59e0b'}"></span></div><span style="min-width:64px;text-align:right;font-weight:700">${Math.round(wa)}/${100-Math.round(wa)}</span></div>`;}).join('')}</div> ${editable?`<div class="pick-btns"><button class="btn-ghost" onclick="setWinner(${r},${i},${JSON.stringify(a)});closeModal()">Advance ${a}</button><button class="btn-ghost" onclick="setWinner(${r},${i},${JSON.stringify(b)});closeModal()">Advance ${b}</button></div>`:''} <button class="btn-primary modal-close" onclick="closeModal()">Close</button>`; document.getElementById('overlay').classList.add('open'); } function enterScore(r,i){const h=+document.getElementById('scH').value,a=+document.getElementById('scA').value;const m=rounds[r][i];let winner=h>a?m.a:(a>h?m.b:(winProb(m.a,m.b)>=.5?m.a:m.b));m.result={h,a,winner,pen:h===a?'model':null};m.ft=true;m.live=false;m.winner=winner;propagate();savePicks();render();closeModal();flash(`โœ… ${m.a} ${h}โ€“${a} ${m.b} recorded.`);} function closeModal(){document.getElementById('overlay').classList.remove('open');} function seed(name,s){let h=s*131;for(const c of name)h=(h*31+c.charCodeAt(0))%9973;return(h%100)/100;}

function simulateOnce(){let layer=rounds[0].map(m=>m.ft?m.winner:(Math.random()<adjProb(m.a,m.b)?m.a:m.b));const reach={};layer.forEach(t=>reach[t]=1);let rd=1;while(layer.length>1){const nx=[];for(let i=0;i<layer.length;i+=2){const a=layer[i],b=layer[i+1];nx.push(Math.random()<adjProb(a,b)?a:b);}layer=nx;rd++;layer.forEach(t=>reach[t]=rd);}reach[layer[0]]=5;return reach;} function runMonteCarlo(runs){const info=document.getElementById('runinfo');info.innerHTML=`<span class="loading">Running ${runs.toLocaleString()} simulationsโ€ฆ</span>`;setTimeout(()=>{const counts={};Object.keys(T).forEach(t=>counts[t]=[0,0,0,0,0]);for(let n=0;n<runs;n++){const reach=simulateOnce();for(const t in reach){const mx=reach[t];for(let lvl=1;lvl<=mx;lvl++)counts[t][lvl-1]++;}}const rows=Object.keys(T).map(t=>({t,p:counts[t].map(c=>c/runs)})).sort((a,b)=>b.p[4]-a.p[4]);let html=`<thead><tr><th>Team</th>${reachLabels.map(l=>`<th class="num">${l}</th>`).join('')}</tr></thead><tbody>`;rows.forEach(r=>{html+=`<tr><td class="tname">${T[r.t].f} ${r.t}</td>`;r.p.forEach((v,idx)=>{const col=idx===4?'#f59e0b':'#6366f1';html+=`<td class="num cellbar"><div class="fill" style="background:${col};width:${v*100}%"></div><span>${(v*100).toFixed(1)}%</span></td>`;});html+='</tr>';});html+='</tbody>';document.getElementById('probTable').innerHTML=html;info.textContent=`${runs.toLocaleString()} simulations complete @ ${Math.round(upsetRate*100)}% upset rate.`;},30);}

function runBestWorst(){const team=document.getElementById('bwTeam').value;const runs=6000;const finishes=[];let champ=0;for(let n=0;n<runs;n++){const reach=simulateOnce();finishes.push(reach[team]||0);if(reach[team]===5)champ++;}finishes.sort((a,b)=>a-b);const pct=q=>finishes[Math.floor(q*finishes.length)];const fmt=v=>["Out (R32)","Reached R16","Reached QF","Reached SF","Runner-up","๐Ÿ† Champion"][v];document.getElementById('floorCeil').innerHTML=`<div class="fc"><div class="lab">๐Ÿ’€ Floor (5th %ile)</div><div class="res" style="color:var(--lose)">${fmt(pct(0.05))}</div></div><div class="fc"><div class="lab">๐Ÿ“ Median</div><div class="res" style="color:var(--primary-hi)">${fmt(pct(0.5))}</div></div><div class="fc"><div class="lab">๐Ÿš€ Ceiling (95th %ile)</div><div class="res" style="color:var(--win)">${fmt(pct(0.95))}</div></div><div class="fc"><div class="lab">๐Ÿ† Title odds</div><div class="res" style="color:var(--gold)">${(champ/runs*100).toFixed(1)}%</div></div>`;const slot=R32.findIndex(m=>m[0]===team||m[1]===team);const best=computePath(team,slot,'easy'),worst=computePath(team,slot,'hard');const ph=(p,cls)=>p.map((s,i)=>`<div class="pathstep"><span class="rd">${roundNames[i]}</span><span class="opp">${T[s.opp].f} ${s.opp}</span><span class="pp" style="color:${cls==='best'?'#10b981':'#ef4444'}">${Math.round(winProb(team,s.opp)*100)}%</span></div>`).join('');document.getElementById('bwGrid').innerHTML=`<div class="bw-card best"><h4>๐ŸŸข Best Case Path <span style="color:var(--muted);font-weight:500;font-size:11px">(weakest opponents)</span></h4>${ph(best,'best')}<div style="margin-top:10px;font-size:12px;color:var(--muted)">Combined opp rating: <b style="color:var(--win)">${best.reduce((a,s)=>a+T[s.opp].r,0).toLocaleString()}</b></div></div><div class="bw-card worst"><h4>๐Ÿ”ด Worst Case Path <span style="color:var(--muted);font-weight:500;font-size:11px">(toughest opponents)</span></h4>${ph(worst,'worst')}<div style="margin-top:10px;font-size:12px;color:var(--muted)">Combined opp rating: <b style="color:var(--lose)">${worst.reduce((a,s)=>a+T[s.opp].r,0).toLocaleString()}</b></div></div>`;} function computePath(team,slot,mode){const path=[];for(let r=0;r<5;r++){const bs=Math.pow(2,r+1);const start=Math.floor(slot/bs)*bs;const half=bs/2;const first=(slot-start)<half;const os=first?start+half:start;const c=[];for(let s=os;s<os+half;s++){c.push(R32[s][0],R32[s][1]);}const sorted=c.filter(x=>x!==team).sort((a,b)=>T[a].r-T[b].r);path.push({opp:mode==='easy'?sorted[0]:sorted[sorted.length-1]});}return path;}

function openDrawer(){document.getElementById('drawer').classList.add('open');document.getElementById('drawerBg').classList.add('open');loadSettingsToForm();} function closeDrawer(){document.getElementById('drawer').classList.remove('open');document.getElementById('drawerBg').classList.remove('open');} function onSourceChange(){const s=document.getElementById('cfgSource').value;document.getElementById('keyField').style.display=(s==='footballdata'||s==='apifootball')?'block':'none';document.getElementById('proxyField').style.display=(s==='proxy')?'block':'none';} function setMode(m){cfg.mode=m;document.querySelectorAll('#modeSeg button').forEach(b=>b.classList.toggle('on',b.dataset.mode===m));document.getElementById('fixedField').style.display=(m==='fixed')?'block':'none';const hints={adaptive:"Adaptive = poll fast only when a game is live/near. Best for quota.",fixed:"Fixed = poll every N minutes regardless of schedule.",off:"Off = no automatic polling. Use Refresh now / manual entry."};document.getElementById('modeHint').textContent=hints[m];} function loadSettingsToForm(){document.getElementById('cfgSource').value=cfg.source;document.getElementById('cfgKey').value=cfg.key;document.getElementById('cfgProxy').value=cfg.proxy;document.getElementById('cfgInterval').value=cfg.interval;setMode(cfg.mode);onSourceChange();} function saveSettings(){cfg.source=document.getElementById('cfgSource').value;cfg.key=document.getElementById('cfgKey').value.trim();cfg.proxy=document.getElementById('cfgProxy').value.trim();cfg.interval=Math.max(1,+document.getElementById('cfgInterval').value||5);try{localStorage.setItem('wc2026cfg',JSON.stringify(cfg));}catch(e){}adminLog("๐Ÿ’พ Settings saved.");scheduleNextPoll();updateLivePill();flash("๐Ÿ’พ Settings saved & applied.");} function loadSettings(){try{const s=localStorage.getItem('wc2026cfg');if(s)cfg=Object.assign(cfg,JSON.parse(s));}catch(e){}} function adminLog(msg){const el=document.getElementById('adminLog');if(el)el.textContent=new Date().toLocaleTimeString()+" "+msg;} function liveLog(msg){const el=document.getElementById('liveLog');if(el)el.textContent=new Date().toLocaleTimeString()+" "+msg;}

function adaptiveIntervalMs(){ const now=Date.now();let best=Infinity; rounds.flat().forEach(m=>{ if(!m.iso||m.ft)return; const k=new Date(m.iso).getTime();const diff=k-now; if(diff<=0&&diff>-120*60000)best=Math.min(best,75*1000); else if(diff>0&&diff<=30*60000)best=Math.min(best,5*60000); else if(diff<=-120*60000&&diff>-130*60000)best=Math.min(best,2*60000); }); return best===Infinity?0:best; } function scheduleNextPoll(){ clearTimeout(pollTimer);clearInterval(countdownTimer); if(cfg.mode==='off'){nextPollAt=0;updateNextLabel();updateLivePill();return;} let ms=(cfg.mode==='fixed')?cfg.interval*60000:adaptiveIntervalMs(); if(!ms){nextPollAt=0;updateNextLabel();updateLivePill();pollTimer=setTimeout(scheduleNextPoll,5*60000);return;} nextPollAt=Date.now()+ms; pollTimer=setTimeout(()=>{pollNow();scheduleNextPoll();},ms); countdownTimer=setInterval(updateNextLabel,1000); updateNextLabel();updateLivePill(); } function updateNextLabel(){ const el=document.getElementById('nextPoll'); if(!nextPollAt){el.textContent=cfg.mode==='off'?'ยท auto-refresh off':'ยท idle (no game near)';return;} const s=Math.max(0,Math.round((nextPollAt-Date.now())/1000)); el.innerHTML=`ยท next refresh in <b>${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}</b> (${cfg.mode})`; } function updateLivePill(){ const anyLive=rounds.flat().some(m=>m.live&&!m.ft); const pill=document.getElementById('livePill'),txt=document.getElementById('liveStatusText'); if(anyLive){pill.classList.remove('off');txt.textContent="LIVE ยท match in play";} else if(cfg.mode==='off'){pill.classList.add('off');txt.textContent="AUTO-REFRESH OFF";} else{pill.classList.add('off');txt.textContent="WATCHING ยท "+(cfg.source==='manual'?'manual':cfg.mode);} } async function pollNow(){ document.getElementById('lastUpdated').textContent=new Date().toLocaleTimeString(); if(cfg.source==='manual'){liveLog("Manual mode โ€” no API call. Enter scores via match modal.");adminLog("Manual mode: nothing to fetch.");updateLivePill();return;} try{ let matches=[]; if(cfg.source==='footballdata'){ if(!cfg.key){liveLog("No API key set (Admin).");return;} const res=await fetch("https://api.football-data.org/v4/competitions/WC/matches",{headers:{"X-Auth-Token":cfg.key}}); if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(d.matches||[]).map(m=>({home:norm(m.homeTeam&&m.homeTeam.name),away:norm(m.awayTeam&&m.awayTeam.name),hs:m.score&&m.score.fullTime&&m.score.fullTime.home,as:m.score&&m.score.fullTime&&m.score.fullTime.away,status:m.status})); }else if(cfg.source==='apifootball'){ if(!cfg.key){liveLog("No API key set (Admin).");return;} const res=await fetch("https://v3.football.api-sports.io/fixtures?league=1&season=2026",{headers:{"x-apisports-key":cfg.key}}); if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(d.response||[]).map(x=>({home:norm(x.teams&&x.teams.home&&x.teams.home.name),away:norm(x.teams&&x.teams.away&&x.teams.away.name),hs:x.goals&&x.goals.home,as:x.goals&&x.goals.away,status:x.fixture&&x.fixture.status&&x.fixture.status.short})); }else if(cfg.source==='proxy'){ if(!cfg.proxy){liveLog("No proxy URL set (Admin).");return;} const res=await fetch(cfg.proxy);if(!res.ok)throw new Error("HTTP "+res.status); const d=await res.json();matches=(Array.isArray(d)?d:d.matches||[]).map(m=>({home:norm(m.home),away:norm(m.away),hs:m.homeScore,as:m.awayScore,status:m.status})); } const applied=applyFeed(matches); liveLog(`โœ… Fetched ${matches.length} matches, applied ${applied}.`);adminLog(`โœ… ${applied} updates applied.`); }catch(e){ liveLog(`โŒ ${e.message}. Browser CORS may block direct API calls โ€” use a proxy URL for production.`); adminLog(`โŒ Fetch failed: ${e.message}`); } scheduleNextPoll();updateLivePill(); } function applyFeed(matches){ let applied=0; matches.forEach(fm=>{ if(!fm.home||!fm.away)return; const idx=rounds[0].findIndex(x=>(x.a===fm.home&&x.b===fm.away)||(x.a===fm.away&&x.b===fm.home)); if(idx<0)return;const m=rounds[0][idx]; if(m.ft)return; // never overwrite a locked real result const swap=m.a!==fm.home; const HH=swap?fm.as:fm.hs, AA=swap?fm.hs:fm.as; const live=["IN_PLAY","PAUSED","1H","2H","HT","ET","LIVE"].includes(fm.status); const done=["FINISHED","FT","AET","PEN"].includes(fm.status); if(done&&HH!=null&&AA!=null){const w=HH>AA?m.a:(AA>HH?m.b:m.a);m.result={h:HH,a:AA,winner:w};m.ft=true;m.live=false;m.winner=w;applied++;} else if(live&&HH!=null&&AA!=null){m.live=true;m.liveScore={h:HH,a:AA};applied++;} }); if(applied){propagate();render();} return applied; } function norm(n){if(!n)return n;const map={"United States":"USA","USA":"USA","Ivory Coast":"Cรดte d'Ivoire","Cรดte d'Ivoire":"Cรดte d'Ivoire","Congo DR":"DR Congo","DR Congo":"DR Congo","Cape Verde":"Cabo Verde","Cabo Verde":"Cabo Verde","Bosnia-Herzegovina":"Bosnia and Herzegovina","Bosnia and Herzegovina":"Bosnia and Herzegovina"};return map[n]||n;}

function savePicks(){try{const picks=rounds.map(rd=>rd.map(m=>m.winner));localStorage.setItem('wc2026picks',JSON.stringify(picks));}catch(e){}} function applySaved(){try{const s=localStorage.getItem('wc2026picks');if(!s)return;const picks=JSON.parse(s);for(let r=0;r<rounds.length;r++){rounds[r].forEach((m,i)=>{const w=picks[r]&&picks[r][i];if(w&&!m.ft&&(w===m.a||w===m.b))m.winner=w;});propagate();}}catch(e){}} function clearSavedBracket(){try{localStorage.removeItem('wc2026picks');}catch(e){}buildInitial();render();adminLog("๐Ÿ—‘๏ธ Saved picks cleared.");flash("๐Ÿ—‘๏ธ Saved picks cleared.");}

function updateStats(){const all=rounds.flat();const dec=all.filter(m=>m.winner).length;document.getElementById('stFilled').textContent=`${dec}/${all.length}`;const ups=all.filter(m=>m.winner&&m.a&&m.b&&isUpset(m.winner,m.winner===m.a?m.b:m.a)).length;document.getElementById('stUpsets').textContent=ups;const champ=rounds[4][0].winner;if(champ){document.getElementById('stChamp').textContent=champ;document.getElementById('stOdds').textContent=quickOdds(champ)+'%';}else{document.getElementById('stChamp').textContent='โ€”';document.getElementById('stOdds').textContent='โ€”';}} function quickOdds(team){let w=0,runs=1500;for(let n=0;n<runs;n++){if(simulateOnce()[team]===5)w++;}return Math.round(w/runs*100);} function switchTab(tab){document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active',p.id==='panel-'+tab));} function flash(msg){let f=document.getElementById('flash');if(!f){f=document.createElement('div');f.id='flash';f.style.cssText='position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--surface2);border:1px solid var(--border);padding:11px 18px;border-radius:11px;font-size:12.5px;font-weight:600;box-shadow:0 8px 30px rgba(0,0,0,.4);z-index:99;transition:opacity .3s;max-width:90vw;text-align:center;';document.body.appendChild(f);}f.textContent=msg;f.style.opacity='1';clearTimeout(f._t);f._t=setTimeout(()=>f.style.opacity='0',3000);}

(function(){const sel=document.getElementById('bwTeam');Object.keys(T).sort((a,b)=>T[b].r-T[a].r).forEach(t=>{const o=document.createElement('option');o.value=t;o.textContent=t;sel.appendChild(o);});})(); loadSettings();buildInitial();applySaved();render();scheduleNextPoll(); document.getElementById('lastUpdated').textContent=new Date().toLocaleTimeString(); </script>

How I Built an AI World Cup Simulator to Settle Family Arguments - No Coding | TrioNx AI