roundMatches = {
if (!selectedRound) return null
const roundValue = selectedRound.value
// "All Rounds" mode for Results/Fixtures
if (roundValue === "all") {
let filtered = seasonPreds
.filter(m => {
const finished = _isFinished(m, m.round)
return matchView === "Results" ? finished : !finished
})
// Include fixture-only rounds (no predictions yet)
if (seasonFixtures) {
const predRounds = new Set(seasonPreds.map(d => `${d.round}|${predToFull[d.home_team] || d.home_team}|${predToFull[d.away_team] || d.away_team}`))
const fixtureOnly = seasonFixtures
.filter(f => {
const h = predToFull[squiggleToPred[f.hteam] || f.hteam] || predToFull[f.hteam] || f.hteam
const a = predToFull[squiggleToPred[f.ateam] || f.ateam] || predToFull[f.ateam] || f.ateam
if (predRounds.has(`${f.round}|${h}|${a}`)) return false
return matchView === "Results" ? f.complete === 100 : f.complete < 100
})
.map(f => ({
home_team: predToFull[squiggleToPred[f.hteam] || f.hteam] || predToFull[f.hteam] || f.hteam,
away_team: predToFull[squiggleToPred[f.ateam] || f.ateam] || predToFull[f.ateam] || f.ateam,
round: f.round,
fixture: f,
fixtureOnly: true
}))
filtered = [...filtered, ...fixtureOnly]
}
// Results: most recent round first; Fixtures: next round first
const dir = matchView === "Results" ? -1 : 1
filtered.sort((a, b) => {
if (a.round !== b.round) return (a.round - b.round) * dir
const fa = a.fixture || lookupFixture(a.round, a.home_team, a.away_team)
const fb = b.fixture || lookupFixture(b.round, b.home_team, b.away_team)
if (fa && fb) return (fa.unixtime || 0) - (fb.unixtime || 0)
return 0
})
return filtered
}
// Single round mode
const round = roundValue
const predMatches = seasonPreds.filter(d => d.round === round)
// For fixture-only rounds, build cards from Squiggle data
if (predMatches.length === 0 && seasonFixtures) {
const fixMatches = seasonFixtures.filter(d => d.round === round)
.sort((a, b) => (a.unixtime || 0) - (b.unixtime || 0))
if (fixMatches.length > 0) return fixMatches.map(f => ({
home_team: predToFull[squiggleToPred[f.hteam] || f.hteam] || predToFull[f.hteam] || f.hteam,
away_team: predToFull[squiggleToPred[f.ateam] || f.ateam] || predToFull[f.ateam] || f.ateam,
round: f.round,
fixture: f,
fixtureOnly: true
}))
}
// Round view shows all; Results/Fixtures filter by status
const filtered = matchView === "Round"
? predMatches
: predMatches.filter(m => {
const finished = _isFinished(m, round)
return matchView === "Results" ? finished : !finished
})
// Sort by fixture kickoff time when available
return filtered.sort((a, b) => {
const fa = lookupFixture(round, a.home_team, a.away_team)
const fb = lookupFixture(round, b.home_team, b.away_team)
if (fa && fb) return (fa.unixtime || 0) - (fb.unixtime || 0)
return Math.abs(b.pred_margin) - Math.abs(a.pred_margin)
})
}
formatMatchDate = (dateStr) => window.formatMatchDate(dateStr, "short")
// Team form from played matches this season (stores result + match link)
aflTeamForm = {
if (!seasonPreds) return {}
const form = {}
const played = seasonPreds
.filter(d => _isFinished(d, d.round))
.sort((a, b) => (a.round || 0) - (b.round || 0))
for (const m of played) {
const hKey = m.home_team
const aKey = m.away_team
if (!form[hKey]) form[hKey] = []
if (!form[aKey]) form[aKey] = []
const url = `match#season=${selectedSeason}&round=${m.round}&home=${teamToAbbr(m.home_team)}&away=${teamToAbbr(m.away_team)}`
// Check actual_margin first, fall back to fixture scores
let hResult, aResult, hScore, aScore
const homeFull = predToFull[m.home_team] || m.home_team
const awayFull = predToFull[m.away_team] || m.away_team
const f = lookupFixture(m.round, m.home_team, m.away_team)
if (m.actual_margin != null && !isNaN(m.actual_margin)) {
hResult = m.actual_margin > 0 ? "W" : m.actual_margin < 0 ? "L" : "D"
aResult = m.actual_margin > 0 ? "L" : m.actual_margin < 0 ? "W" : "D"
hScore = f?.hscore ?? (m.pred_total != null ? Math.round((m.pred_total + m.actual_margin) / 2) : null)
aScore = f?.ascore ?? (m.pred_total != null ? Math.round((m.pred_total - m.actual_margin) / 2) : null)
} else if (f && f.hscore != null) {
hResult = f.hscore > f.ascore ? "W" : f.hscore < f.ascore ? "L" : "D"
aResult = f.hscore > f.ascore ? "L" : f.hscore < f.ascore ? "W" : "D"
hScore = f.hscore; aScore = f.ascore
}
if (hResult) {
const hAbbr = teamToAbbr(m.home_team), aAbbr = teamToAbbr(m.away_team)
const scoreStr = hScore != null ? ` ${hScore}–${aScore}` : ""
form[hKey].push({ result: hResult, url, tip: `${hAbbr} v ${aAbbr}${scoreStr} ${hResult}` })
form[aKey].push({ result: aResult, url, tip: `${hAbbr} v ${aAbbr}${scoreStr} ${aResult}` })
}
}
for (const k of Object.keys(form)) form[k] = form[k].slice(-5)
return form
}
aflFormDots = (team) => {
const results = aflTeamForm[team]
if (!results || results.length === 0) return ""
return `<span class="form-dots">${results.map(r =>
`<span class="form-dot form-${r.result.toLowerCase()}" data-tip="${statsEsc(r.tip || "")}" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='${r.url}'"></span>`
).join("")}</span>`
}
matchCards = {
if (!roundMatches || roundMatches.length === 0) {
const msg = matchView === "Results"
? "No results available."
: matchView === "Fixtures"
? "No upcoming fixtures."
: "No matches available for this round."
return html`<p class="text-muted">${msg}</p>`
}
const showForm = matchView !== "Results"
const allRoundsMode = selectedRound.value === "all"
const badge = (name) => {
const src = teamLogo(name)
return src ? `<img src="${src}" alt="" class="team-badge" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='team#team=${encodeURIComponent(name)}'">` : ""
}
const buildCard = (m, round) => {
const homeName = predToFull[m.home_team] || m.home_team
const awayName = predToFull[m.away_team] || m.away_team
const matchUrl = `match#season=${selectedSeason}&round=${round}&home=${teamToAbbr(m.home_team)}&away=${teamToAbbr(m.away_team)}`
// Fixture-only card (no TORP predictions yet)
if (m.fixtureOnly) {
const f = m.fixture
const played = f.complete === 100
const isLive = f.complete > 0 && f.complete < 100
const liveClass = isLive ? " is-live" : ""
const liveBadge = isLive ? `<span class="live-badge">LIVE</span>` : ""
const liveQuarter = isLive && f.timestr ? `<div class="match-quarter">${statsEsc(f.timestr)}</div>` : ""
const scoreHtml = (played || isLive)
? `<div class="match-score">${f.hscore ?? 0} – ${f.ascore ?? 0}${liveQuarter}</div>`
: ""
return `
<a href="${matchUrl}" class="match-card-link">
<div class="match-card fixture-only${liveClass}" data-match-id="${f.id}">
<div class="match-info">${liveBadge}${statsEsc(formatMatchDate(f.date))}${f.venue ? ` · ${statsEsc(f.venue)}` : ""}</div>
<div class="match-teams">
<div class="team">${badge(homeName)}<span class="team-link" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='team#team=${encodeURIComponent(homeName)}'">${statsEsc(homeName)}</span></div>
<div class="match-vs">${(played || isLive) ? scoreHtml : "vs"}</div>
<div class="team">${badge(awayName)}<span class="team-link" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='team#team=${encodeURIComponent(awayName)}'">${statsEsc(awayName)}</span></div>
</div>
</div>
</a>`
}
// Standard prediction card — enrich with fixture data
const f = lookupFixture(m.round, m.home_team, m.away_team)
const homeWin = m.pred_margin > 0
const played = m.actual_margin != null && !isNaN(m.actual_margin)
const isLive = f && f.complete > 0 && f.complete < 100
const liveClass = isLive ? " is-live" : ""
const liveBadge = isLive ? `<span class="live-badge">LIVE</span>` : ""
const matchIdAttr = f ? ` data-match-id="${f.id}"` : ""
const infoHtml = f
? `<div class="match-info">${liveBadge}${statsEsc(formatMatchDate(f.date))}${f.venue ? ` · ${statsEsc(f.venue)}` : ""}</div>`
: ""
const squigglePlayed = f && f.complete === 100
const isFinished = played || squigglePlayed
const hasXs = m.xscore_home != null && m.xscore_away != null
const predHome = m.pred_total != null && m.pred_margin != null ? Math.round((m.pred_total + m.pred_margin) / 2) : null
const predAway = m.pred_total != null && m.pred_margin != null ? Math.round((m.pred_total - m.pred_margin) / 2) : null
let scoreHtml = "vs"
if (isLive) {
const liveQuarter = f.timestr ? `<div class="match-quarter">${statsEsc(f.timestr)}</div>` : ""
scoreHtml = `<div class="match-score">${f.hscore ?? 0} – ${f.ascore ?? 0}${liveQuarter}</div>`
} else if (squigglePlayed) {
const xsLine = hasXs ? `<div class="match-xg">xS: ${m.xscore_home.toFixed(0)} – ${m.xscore_away.toFixed(0)}</div>` : ""
scoreHtml = `<div class="match-score">${f.hscore} – ${f.ascore}${xsLine}</div>`
} else if (played) {
const homeScore = m.pred_total != null ? Math.round((m.pred_total + m.actual_margin) / 2) : null
const awayScore = m.pred_total != null ? Math.round((m.pred_total - m.actual_margin) / 2) : null
const xsLine = hasXs ? `<div class="match-xg">xS: ${m.xscore_home.toFixed(0)} – ${m.xscore_away.toFixed(0)}</div>` : ""
if (homeScore != null && awayScore != null) {
scoreHtml = `<div class="match-score">${homeScore} – ${awayScore}${xsLine}</div>`
}
}
const dualBar = isFinished && hasXs
let xsBarHtml = ""
if (dualBar) {
const xsMargin = m.xscore_home - m.xscore_away
const xsHomeProb = 1 / (1 + Math.exp(-xsMargin / 20))
const xsHomePct = (xsHomeProb * 100).toFixed(0)
const xsAwayPct = ((1 - xsHomeProb) * 100).toFixed(0)
xsBarHtml = `
<div class="prob-bar-row">
<span class="prob-bar-label">xS</span>
<div class="prob-bar-container">
<div class="prob-bar prob-home" style="width: ${xsHomeProb * 100}%">
${xsHomeProb >= 0.15 ? xsHomePct + '%' : ''}
</div>
<div class="prob-bar prob-away" style="width: ${(1 - xsHomeProb) * 100}%">
${(1 - xsHomeProb) >= 0.15 ? xsAwayPct + '%' : ''}
</div>
</div>
</div>`
}
let liveWpBarHtml = ""
if (isLive && window.aflMatchProbs) {
const probs = window.aflMatchProbs(f.timestr, f.complete, f.hscore, f.ascore)
const hPct = (probs.home * 100).toFixed(0)
const dPct = (probs.draw * 100).toFixed(0)
const aPct = (probs.away * 100).toFixed(0)
const showDraw = probs.draw >= 0.02
liveWpBarHtml = `
<div class="prob-bar-row" data-wp-bar>
<span class="prob-bar-label">Live</span>
<div class="prob-bar-container">
<div class="prob-bar prob-home" style="width: ${probs.home * 100}%">
${probs.home >= 0.15 ? hPct + '%' : ''}
</div>
${showDraw ? `<div class="prob-bar prob-draw" style="width: ${probs.draw * 100}%">
${probs.draw >= 0.05 ? dPct + '%' : ''}
</div>` : ''}
<div class="prob-bar prob-away" style="width: ${(showDraw ? probs.away : 1 - probs.home) * 100}%">
${probs.away >= 0.15 ? aPct + '%' : ''}
</div>
</div>
</div>`
}
const homeRating = m.pred_total != null ? `<span class="rating">${predHome}</span>` : ""
const awayRating = m.pred_total != null ? `<span class="rating">${predAway}</span>` : ""
return `
<a href="${matchUrl}" class="match-card-link">
<div class="match-card${liveClass}"${matchIdAttr}>
${infoHtml}
<div class="match-teams">
<div class="team ${homeWin ? 'favoured' : ''}">${badge(homeName)}<span class="team-link" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='team#team=${encodeURIComponent(homeName)}'">${statsEsc(homeName)}</span>
${homeRating}
${showForm ? aflFormDots(m.home_team) : ""}
</div>
<div class="match-vs">${scoreHtml}</div>
<div class="team ${!homeWin ? 'favoured' : ''}">${badge(awayName)}<span class="team-link" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='team#team=${encodeURIComponent(awayName)}'">${statsEsc(awayName)}</span>
${awayRating}
${showForm ? aflFormDots(m.away_team) : ""}
</div>
</div>
<div class="match-prediction">
<div class="prob-bars-group">
<div class="prob-bar-row">
${(dualBar || isLive) ? '<span class="prob-bar-label">TORP</span>' : ''}
<div class="prob-bar-container">
<div class="prob-bar prob-home" style="width: ${m.home_win_prob * 100}%">
${m.home_win_prob >= 0.15 ? (m.home_win_prob * 100).toFixed(0) + '%' : ''}
</div>
<div class="prob-bar prob-away" style="width: ${(1 - m.home_win_prob) * 100}%">
${(1 - m.home_win_prob) >= 0.15 ? ((1 - m.home_win_prob) * 100).toFixed(0) + '%' : ''}
</div>
</div>
</div>
${xsBarHtml}
${liveWpBarHtml}
</div>
</div>
<div class="match-card-footer">
${!isFinished && !isLive && predHome != null ? `<div class="pred-summary">Prediction: ${predHome === predAway ? "Draw" : `${homeWin ? statsEsc(homeName) : statsEsc(awayName)} by ${Math.abs(predHome - predAway)} pts`}</div>` : ""}
<div class="match-chain-link"><span class="chain-nav" role="link" tabindex="0" onclick="event.preventDefault();event.stopPropagation();window.location.href='match-chains.html#season=${selectedSeason}&round=${round}&home=${teamToAbbr(m.home_team)}&away=${teamToAbbr(m.away_team)}'">Chains →</span></div>
</div>
</div>
</a>`
}
// Build cards with round group separators in all-rounds mode
const parts = []
let lastRound = null
for (const m of roundMatches) {
const round = m.round ?? selectedRound.value
if (allRoundsMode && round !== lastRound) {
const label = round === 0 ? "Opening Round" : `Round ${round}`
parts.push(`<div class="round-group-header">${label}</div>`)
lastRound = round
}
parts.push(buildCard(m, round))
}
return html`<div class="match-cards-container">${parts.join('')}</div>`
}