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>'