Spaces:
Running
Running
File size: 10,039 Bytes
8d91c6f 4a8ef44 8d91c6f 4a8ef44 8d91c6f 4a8ef44 8d91c6f 4a8ef44 8d91c6f 4a8ef44 4484c39 8d91c6f 4484c39 8d91c6f 4a8ef44 4484c39 d8b324c 4484c39 c41ac50 4484c39 8d91c6f b9b3be5 8d91c6f b9b3be5 8d91c6f 4484c39 4a8ef44 8d91c6f 4a8ef44 8d91c6f 4484c39 8d91c6f 978c431 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | """HTML rendering of the boards (pure strings, no JS)."""
from __future__ import annotations
import html
from tetris import COLOR_ID, HEIGHT, ROTATIONS, 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; align-items:center; gap:6px; min-height:18px;}
/* ---- 8-bit game look: square bevelled blocks, black well, grey frame, pixel font (like the original) ---- */
.pstats {display:grid; grid-template-columns:repeat(3,1fr); gap:4px;}
.pstats div {background:#000; border:2px solid #6b6b6b; box-shadow:inset 0 0 0 1px #000, 0 0 0 1px #000;
padding:5px 2px 4px; text-align:center; min-width:0;}
.pstats b {display:block; font:400 10px/1.25 'Press Start 2P',ui-monospace,monospace; color:#fff; letter-spacing:-.02em;
overflow:hidden; text-overflow:clip; white-space:nowrap;}
.pstats span {display:block; margin-top:3px; font:400 6px/1 'Press Start 2P',ui-monospace,monospace; text-transform:uppercase;
color:#9a9a9a;}
.board {position:relative; display:grid; grid-template-columns:repeat(10,1fr); grid-template-rows:repeat(20,1fr); gap:0;
padding:0; background:#000; border:3px solid #a8a8a8; border-radius:0; aspect-ratio:1/2;
box-shadow:0 0 0 2px #000, 0 0 0 4px #5a5a5a; margin:4px; image-rendering:pixelated;}
.board i {display:block; background:#000; box-shadow:inset 0 0 0 1px #0c0c0c;}
.board i[class^="c"] {box-shadow:inset 2px 2px 0 rgba(255,255,255,.55), inset -2px -2px 0 rgba(0,0,0,.5), inset 0 0 0 1px rgba(0,0,0,.65);}
.board i.c1, .pnext i.c1 {background:#00d8f0;} .board i.c2, .pnext i.c2 {background:#f0d800;} .board i.c3, .pnext i.c3 {background:#a000f0;}
.board i.c4, .pnext i.c4 {background:#00c800;} .board i.c5, .pnext i.c5 {background:#e80000;} .board i.c6, .pnext i.c6 {background:#1840ff;}
.board i.c7, .pnext i.c7 {background:#f08c00;}
.board i.n {filter:brightness(1.35); animation:px-flash .4s steps(2) 1;}
@keyframes px-flash {0% {filter:brightness(2.2);} 100% {filter:brightness(1.35);}}
.board .over {position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
background:rgba(0,0,0,.74); gap:8px; text-align:center; padding:6px;
font:400 11px/1.4 'Press Start 2P',ui-monospace,monospace;}
.board .over, .board .over * {color:#fff !important; opacity:1 !important;}
.board .over .medal {font-size:44px; line-height:1; font-family:initial;}
.board .over small {font:600 12px/1.3 'DM Sans',Arial,sans-serif;}
.pnext {display:inline-flex; align-items:center; gap:5px; font:400 7px/1 'Press Start 2P',ui-monospace,monospace; color:var(--body-text-color-subdued);}
.pnext .mini {display:inline-grid; gap:0; background:#000; padding:2px; border:1px solid #6b6b6b;}
.pnext .mini i {display:block; width:6px; height:6px;}
.pnext .mini i[class^="c"] {box-shadow:inset 1px 1px 0 rgba(255,255,255,.55), inset -1px -1px 0 rgba(0,0,0,.5);}
@media (prefers-reduced-motion:reduce) {.board i.n {animation:none;}}
.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 _next_html(piece) -> str:
"""Tiny NEXT box with the upcoming piece, like the original game's side panel."""
cells = ROTATIONS[piece][0]
rows = 1 + max(r for r, _ in cells)
cols = 1 + max(c for _, c in cells)
filled = set(cells)
color = COLOR_ID[piece]
blocks = "".join(f'<i class="c{color}"></i>' if (r, c) in filled else "<i></i>" for r in range(rows) for c in range(cols))
return (f'<span class="pnext" title="next piece: {piece}">NEXT<span class="mini" '
f'style="grid-template-columns:repeat({cols},6px)">{blocks}</span></span>')
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 = "<small>survived</small>" if game.alive else ""
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>{sub}{elo_txt}</div>'
elif not game.alive:
over = '<div class="over">GAME OVER</div>'
move, tip = _last_move(game, player)
nxt = _next_html(game.current_piece) if game.alive else '<span class="pnext">NEXT –</span>'
return (
f'<div class="pcard"><div class="pname" title="{name}">{name}</div>'
f'<div class="pmeta"><span>{params}</span>{nxt}</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 and elos[i] is not None:
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>'
|