Spaces:
Running
Running
File size: 8,127 Bytes
8d91c6f 4484c39 8d91c6f 4484c39 8d91c6f 4484c39 d8b324c 4484c39 8d91c6f 4484c39 8d91c6f 4484c39 8d91c6f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | """HTML rendering of the boards (pure strings, no JS)."""
from __future__ import annotations
import html
from tetris import HEIGHT, WIDTH
MEDALS = {1: "🥇", 2: "🥈", 3: "🥉"}
CSS = """
.arena {display:grid; grid-template-columns:repeat(auto-fit, minmax(min(128px, 100%), 1fr)); gap:12px 10px; padding:4px 0; max-width:960px; margin:0 auto;}
.pcard {width:100%; max-width:214px; min-width:0; justify-self:center; display:flex; flex-direction:column; gap:6px;}
.pname {font-weight:700; font-size:13px; line-height:1.25; overflow:hidden; text-overflow:ellipsis;
white-space:nowrap; color:var(--body-text-color);}
.pmeta {font-size:11px; color:var(--body-text-color-subdued); display:flex; justify-content:space-between; gap:6px;}
.pstats {display:grid; grid-template-columns:repeat(3,1fr); gap:4px;}
.pstats div {background:var(--block-background-fill); border:1px solid var(--border-color-primary);
border-radius:6px; padding:3px 4px; text-align:center;}
.pstats b {display:block; font-size:15px; color:var(--body-text-color); font-variant-numeric:tabular-nums;}
.pstats span {font-size:10px; text-transform:uppercase; letter-spacing:.04em; color:var(--body-text-color-subdued);}
.board {position:relative; display:grid; grid-template-columns:repeat(10,1fr); gap:1px; padding:3px;
background:#0b1020; border-radius:8px; border:1px solid #273048; aspect-ratio:1/2;}
.board i {display:block; border-radius:2px; background:#141b2e;}
.board i.c1 {background:#22d3ee;} .board i.c2 {background:#facc15;} .board i.c3 {background:#a855f7;}
.board i.c4 {background:#22c55e;} .board i.c5 {background:#ef4444;} .board i.c6 {background:#3b82f6;}
.board i.c7 {background:#f97316;}
.board i.n {box-shadow:inset 0 0 0 2px rgba(255,255,255,.85);}
.board .over {position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
background:rgba(8,10,20,.78); font-weight:800; font-size:18px; border-radius:8px; gap:6px; text-align:center; padding:6px;}
.board .over, .board .over * {color:#fff !important; opacity:1 !important;}
.board .over .medal {font-size:44px; line-height:1;}
.board .over small {font-weight:600; font-size:12px;}
.pmove {font-size:11px; line-height:1.4; color:var(--body-text-color-subdued); min-height:4.3em;
display:flex; flex-direction:column; gap:1px; cursor:help; overflow-wrap:anywhere;}
.pmove .act {color:var(--body-text-color); font-variant-numeric:tabular-nums;}
.pmove b {color:var(--body-text-color);}
.pmove .vg {color:#16a34a; font-weight:700;} .pmove .vb {color:#dc2626; font-weight:700;}
.results {width:100%; max-width:760px; margin:4px auto 0; border-collapse:collapse !important; font-size:13px; border:none !important;}
.results th, .results td {padding:6px 5px !important; border:none !important; border-bottom:1px solid var(--border-color-primary) !important;
text-align:right; color:var(--body-text-color); font-variant-numeric:tabular-nums; background:transparent !important;}
.results td.p {min-width:8.5em;}
@media (max-width:480px) {.results .sm-hide {display:none;} .results {font-size:12px;}}
.results th {font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--body-text-color-subdued);}
.results td.l, .results th.l {text-align:left; overflow-wrap:break-word;}
.results .up {color:#16a34a;} .results .down {color:#dc2626;}
.rnote {font-size:12px; color:var(--body-text-color-subdued); margin:8px auto 0; max-width:760px; line-height:1.45;}
.rnote a {color:var(--link-text-color);}
.arena-empty {text-align:center; padding:40px 12px; color:var(--body-text-color-subdued); font-size:14px;
border:1px dashed var(--border-color-primary); border-radius:10px;}
"""
def _board(game) -> str:
hl = set(game.last_cells)
cells = []
for r in range(HEIGHT):
row = game.grid[r]
for c in range(WIDTH):
v = row[c]
if v:
cells.append(f'<i class="c{v}{" n" if (r, c) in hl else ""}"></i>')
else:
cells.append("<i></i>")
return "".join(cells)
def _last_move(game, player):
"""Two short lines: the action on the board, then the player's answer.
Returns (html, tooltip) where the tooltip is the full outcome description."""
if not game.last_piece:
return "Waiting for the first piece…", ""
action = f"{game.last_piece} → col {game.last_col}"
if game.last_rotations > 1:
action += f" · rot {game.last_rotation + 1}/{game.last_rotations}"
if game.last_lines == 4:
action += " · <b>TETRIS!</b>"
elif game.last_lines:
action += f" · <b>+{game.last_lines} line{'s' if game.last_lines > 1 else ''}</b>"
n = game.last_options
of_n = f" · {n} option{'s' if n != 1 else ''}" if n else ""
pid = getattr(player, "model_id", "")
if pid == "baseline/random":
answer = f"<b>Random:</b> uniform pick{of_n}"
elif pid.startswith("baseline/"):
answer = f"<b>Rules:</b> fixed priorities{of_n}"
elif game.last_value is not None:
v = game.last_value
verdict = "good move" if v > 0 else "bad move"
answer = f'<b>Model:</b> <span class="{"vg" if v > 0 else "vb"}">“{verdict}”</span> {v:+.2f}{of_n}'
else:
answer = ""
return f'<span class="act">{action}</span><span class="ans">{answer}</span>', html.escape(game.last_description)
def player_card(game, player, final_rank=None, elo=None) -> str:
name = html.escape(player.display)
params = "baseline" if not player.n_params else f"{player.n_params / 1e6:.1f}M params"
over = ""
if final_rank is not None:
medal = MEDALS.get(final_rank, f"#{final_rank}")
sub = "survived" if game.alive else "topped out"
elo_txt = ""
if elo is not None:
elo_txt = f"<small>Elo {elo[0]:.0f} → {elo[1]:.0f} ({elo[1] - elo[0]:+.1f})</small>"
over = f'<div class="over"><span class="medal">{medal}</span><small>{sub}</small>{elo_txt}</div>'
elif not game.alive:
over = '<div class="over">GAME OVER</div>'
move, tip = _last_move(game, player)
nxt = game.current_piece if game.alive else "–"
return (
f'<div class="pcard"><div class="pname" title="{name}">{name}</div>'
f'<div class="pmeta"><span>{params}</span><span>next: {nxt}</span></div>'
f'<div class="pstats"><div><b>{game.score}</b><span>score</span></div>'
f'<div><b>{game.lines}</b><span>lines</span></div><div><b>{game.pieces}</b><span>pieces</span></div></div>'
f'<div class="board">{_board(game)}{over}</div>'
f'<div class="pmove" title="{tip}">{move}</div></div>'
)
def arena_html(games, players, ranks=None, elos=None) -> str:
cards = []
for i, (g, p) in enumerate(zip(games, players)):
cards.append(player_card(g, p, ranks[i] if ranks else None, elos[i] if elos else None))
return f'<div class="arena">{"".join(cards)}</div>'
def empty_html(msg="Pick the models and press <b>Start match</b>. The boards will appear here.") -> str:
return f'<div class="arena-empty">{msg}</div>'
def results_html(order, ranks, players, games, elos, note_html) -> str:
rows = []
for i in order:
p, g = players[i], games[i]
if elos:
d = elos[i][1] - elos[i][0]
cls = "up" if d > 0.05 else "down" if d < -0.05 else ""
elo = f'{elos[i][1]:.0f} <span class="{cls}">({d:+.1f})</span>'
else:
elo = "–"
medal = MEDALS.get(ranks[i], str(ranks[i]))
alive = " ✓" if g.alive else ""
rows.append(
f'<tr><td class="l">{medal}</td><td class="l p">{html.escape(p.display).replace("/", "/<wbr>")}</td><td>{g.score}</td>'
f'<td class="sm-hide">{g.lines}</td><td>{g.pieces}{alive}</td><td>{elo}</td></tr>'
)
head = ('<tr><th class="l">#</th><th class="l">Player</th><th>Score</th><th class="sm-hide">Lines</th>'
"<th>Pieces</th><th>Elo</th></tr>")
return f'<table class="results">{head}{"".join(rows)}</table><div class="rnote">{note_html}</div>'
|