Index page: every game with its winner, linking to its replay by game index. For a ladder (games carry a ``group`` matchup label) a Matchup column is shown.
(tour: TournamentInfo)
| 340 | + "<script>" |
| 341 | + renderer.DRAW_JS |
| 342 | + "</script>\n" |
| 343 | + "<script>" |
| 344 | + PLAYER_JS |
| 345 | + "</script>" |
| 346 | ) |
| 347 | return _doc(head, scripts) |
| 348 | |
| 349 | |
| 350 | def build_index(tour: TournamentInfo) -> str: |
| 351 | """Index page: every game with its winner, linking to its replay by game index. |
| 352 | |
| 353 | For a ladder (games carry a ``group`` matchup label) a Matchup column is shown. |
| 354 | """ |
| 355 | players = ", ".join(p["name"] for p in tour.players) if tour.players else "" |
| 356 | laddered = any(g.group for g in tour.games) |
| 357 | rounds = len({g.round for g in tour.games}) |
| 358 | |
| 359 | # Show the per-sim winner (if the arena computed it) as well as the round winner, |
| 360 | # so a decisive individual game isn't mislabeled with its round's aggregate result. |
| 361 | have_sim_winner = any(g.sim_winner is not None or g.sim_draw for g in tour.games) |
| 362 | rows = [] |
| 363 | for idx, g in enumerate(tour.games): |
| 364 | round_winner = g.winner if g.winner is not None else tour.round_winners.get(g.round, "") |
| 365 | sim_winner = "TIE" if g.sim_draw else (g.sim_winner or "") |
| 366 | cell = f'<a class="watch" href="game?g={idx}"><span class="tri">▶</span> watch</a>' |
| 367 | group_td = f"<td>{g.group}</td>" if laddered else "" |
| 368 | sim_td = f"<td>{sim_winner}</td>" if have_sim_winner else "" |
| 369 | rows.append( |
| 370 | f"<tr>{group_td}<td>{g.round}</td><td>{g.sim}</td>{sim_td}<td>{round_winner or ''}</td><td>{cell}</td></tr>" |
| 371 | ) |
| 372 | |
| 373 | style = ( |
| 374 | ":root{--bg:#0d1117;--fg:#e6edf3;--muted:#8b949e;--dim:#6e7681;--line:#21262d;" |
| 375 | "--btn:#21262d;--btnb:#30363d;--accent:#58a6ff}" |
| 376 | ':root[data-theme="light"]{--bg:#ffffff;--fg:#1f2328;--muted:#57606a;--dim:#8c959f;' |
| 377 | "--line:#d0d7de;--btn:#f6f8fa;--btnb:#d0d7de;--accent:#0969da}" |
| 378 | "*{box-sizing:border-box}" |
| 379 | "body{background:var(--bg);color:var(--fg);font:14px system-ui,sans-serif;margin:0;" |
| 380 | "min-height:100vh;display:flex;flex-direction:column}" |
| 381 | ".bar{border-bottom:1px solid var(--line)}" |
| 382 | ".bar-inner{max-width:1000px;margin:0 auto;width:100%;display:flex;align-items:center;" |
| 383 | "justify-content:space-between;padding:0 24px;height:54px}" |
| 384 | ".brand{font-weight:700;font-size:16px}" |
| 385 | "main{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}" |
| 386 | ".wrap{display:flex;gap:32px;align-items:flex-start;max-width:1000px}" |
| 387 | ".info{max-width:220px}" |
| 388 | ".info-title{font-size:16px;font-weight:700;margin-bottom:8px}" |
| 389 | ".folder{color:var(--dim);font-size:12px;margin-bottom:12px;word-break:break-all}" |
| 390 | ".meta{color:var(--muted);font-size:13px;line-height:2}" |
| 391 | ".meta .k{color:var(--dim);display:inline-block;min-width:92px}" |
| 392 | ".games{overflow-y:auto}table{border-collapse:collapse}" |
| 393 | "td,th{padding:5px 14px;border-bottom:1px solid var(--line);text-align:left}" |
| 394 | "th{color:var(--muted);font-weight:600;position:sticky;top:0;background:var(--bg)}" |
| 395 | "a{color:var(--accent)}" |
| 396 | ".btn{background:var(--btn);color:var(--fg);border:1px solid var(--btnb);border-radius:6px;" |
| 397 | "padding:5px 11px;cursor:pointer;font-size:15px;line-height:1;text-decoration:none}" |
| 398 | ".btn:hover{filter:brightness(1.15)}" |
| 399 | ".watch{color:var(--accent);text-decoration:none;font-weight:600;display:inline-flex;align-items:center;gap:6px}" |