Spaces:
Running
Running
Download render.py from DedeProGames/SLM-Tetris-Arena: direct link, hf CLI and curl.
- Browser
- Download file 8.13 kB
-
https://huggingface.co/spaces/DedeProGames/SLM-Tetris-Arena/resolve/d8b324c8e178dbdcba3eec40e1426b631436efa8/render.py
- Command line
-
hf download hf://spaces/DedeProGames/SLM-Tetris-Arena@d8b324c8e178dbdcba3eec40e1426b631436efa8/render.py
-
curl -L -o render.py https://huggingface.co/spaces/DedeProGames/SLM-Tetris-Arena/resolve/d8b324c8e178dbdcba3eec40e1426b631436efa8/render.py
8.13 kB
| """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>' | |