fuzzyworld

word-waltz / files / src / components / Scoreboard.ts

Scoreboard.ts

Blame: the commit that last changed each line · file · history

1c548168 2026-03-24 Pavel Safronov1import type { Player, PlayerId, Round } from '../types.js';
26f9fd72 2026-03-26 Pavel Safronov2import { guessesToEmoji } from '../utils/scoring.js';
f755bca6 2026-03-23 Pavel Safronov3
4/**
5 * Scoreboard — running score display
6 *
1c548168 2026-03-24 Pavel Safronov7 * Shows each player's name and cumulative score, plus the round history.
bf4044de 2026-03-24 Pavel Safronov8 * The current player's name can be edited inline.
9 * The history is collapsed by default (showing last 2 rounds) and can be expanded.
f755bca6 2026-03-23 Pavel Safronov10 */
11
12interface ScoreboardProps {
13 players: { a: Player; b: Player };
14 rounds: Round[];
bf4044de 2026-03-24 Pavel Safronov15 myRole: PlayerId;
4a3e918a 2026-03-26 Pavel Safronov16 turn: 'pick' | 'solve';
17 currentRound: number;
bf4044de 2026-03-24 Pavel Safronov18 onNameChange: (newName: string) => void;
f755bca6 2026-03-23 Pavel Safronov19}
20
21export function mountScoreboard(container: HTMLElement, props: ScoreboardProps): () => void {
4a3e918a 2026-03-26 Pavel Safronov22 const { players, rounds, myRole, turn, currentRound, onNameChange } = props;
1c548168 2026-03-24 Pavel Safronov23
24 const root = document.createElement('div');
25 root.className = 'scoreboard';
26
4a3e918a 2026-03-26 Pavel Safronov27 // Exclude a live round (currently being solved) from the completed list.
28 const liveRoundIdx = turn === 'solve' ? currentRound : -1;
29 const completedRounds = rounds
30 .map((r, i) => ({ r, i }))
31 .filter(({ r, i }) => i !== liveRoundIdx && r.guesses.length > 0);
405e0a58 2026-03-27 Pavel Safronov32 let historyVisible = false;
bf4044de 2026-03-24 Pavel Safronov33
34 function render(): void {
35 root.innerHTML = '';
36
37 // --- Players row ---
38 const playersEl = document.createElement('div');
39 playersEl.className = 'scoreboard__players';
40
41 (['a', 'b'] as PlayerId[]).forEach((id, i) => {
42 if (i === 1) {
43 const vs = document.createElement('span');
44 vs.className = 'scoreboard__vs';
45 vs.textContent = 'vs';
46 playersEl.appendChild(vs);
47 }
48
49 const p: Player = players[id];
50 const displayName = p.name || (id === 'a' ? 'Player A' : 'Player B');
51 const card = document.createElement('div');
52 card.className = 'scoreboard__player';
53
4a3e918a 2026-03-26 Pavel Safronov54 const nameEl = document.createElement('span');
55 nameEl.className = `scoreboard__name scoreboard__name--${id}`;
56 nameEl.textContent = displayName;
57
bf4044de 2026-03-24 Pavel Safronov58 if (id === myRole) {
4a3e918a 2026-03-26 Pavel Safronov59 nameEl.classList.add('scoreboard__name--editable');
60 nameEl.title = 'Click to rename';
61 nameEl.addEventListener('click', () => {
bf4044de 2026-03-24 Pavel Safronov62 const input = document.createElement('input');
63 input.type = 'text';
64 input.className = 'scoreboard__name-input';
65 input.value = p.name;
66 input.maxLength = 20;
4a3e918a 2026-03-26 Pavel Safronov67 nameEl.replaceWith(input);
bf4044de 2026-03-24 Pavel Safronov68 input.focus();
69 input.select();
70
71 function commit() {
72 const val = input.value.trim();
73 if (val && val !== p.name) onNameChange(val);
74 else render(); // revert
75 }
76 input.addEventListener('blur', commit);
77 input.addEventListener('keydown', (e) => {
78 if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
79 if (e.key === 'Escape') { input.removeEventListener('blur', commit); render(); }
80 });
81 });
82 }
1c548168 2026-03-24 Pavel Safronov83
4a3e918a 2026-03-26 Pavel Safronov84 card.appendChild(nameEl);
85
bf4044de 2026-03-24 Pavel Safronov86 const scoreEl = document.createElement('span');
b6b781d4 2026-03-26 Pavel Safronov87 scoreEl.className = `scoreboard__score scoreboard__score--${id}`;
bf4044de 2026-03-24 Pavel Safronov88 scoreEl.textContent = String(p.score);
89 card.appendChild(scoreEl);
90 playersEl.appendChild(card);
91 });
1c548168 2026-03-24 Pavel Safronov92
bf4044de 2026-03-24 Pavel Safronov93 root.appendChild(playersEl);
94
95 // --- History ---
4a3e918a 2026-03-26 Pavel Safronov96 if (completedRounds.length === 0 && liveRoundIdx < 0) return;
bf4044de 2026-03-24 Pavel Safronov97
98 const historyEl = document.createElement('div');
99 historyEl.className = 'scoreboard__history-wrap';
100
405e0a58 2026-03-27 Pavel Safronov101 const toggle = document.createElement('button');
102 toggle.className = 'scoreboard__toggle';
103 toggle.textContent = historyVisible ? '▲ Hide history' : '▼ Show history';
104 toggle.addEventListener('click', () => { historyVisible = !historyVisible; render(); });
105 historyEl.appendChild(toggle);
106
107 if (historyVisible) {
4a3e918a 2026-03-26 Pavel Safronov108
405e0a58 2026-03-27 Pavel Safronov109 const ol = document.createElement('ol');
110 ol.className = 'scoreboard__history';
111 completedRounds.forEach(({ r, i }) => {
112 const roundNum = i + 1;
113 const solver: PlayerId = r.challenger === 'a' ? 'b' : 'a';
4a3e918a 2026-03-26 Pavel Safronov114 const solverName = players[solver].name || (solver === 'a' ? 'Player A' : 'Player B');
405e0a58 2026-03-27 Pavel Safronov115 const result = r.solved
116 ? `solved "${r.word}" in ${r.guesses.length} ${r.guesses.length === 1 ? 'guess' : 'guesses'} (+${r.score})`
117 : `failed to solve "${r.word}"`;
118 const li = document.createElement('li');
119 li.className = 'scoreboard__round';
120 const textEl = document.createElement('span');
4a3e918a 2026-03-26 Pavel Safronov121 const numSpan = document.createElement('span');
122 numSpan.className = 'scoreboard__round-num';
123 numSpan.textContent = `${roundNum}.`;
124 const nameSpan = document.createElement('span');
125 nameSpan.className = `scoreboard__name--${solver}`;
126 nameSpan.textContent = solverName;
405e0a58 2026-03-27 Pavel Safronov127 textEl.appendChild(numSpan);
128 textEl.appendChild(nameSpan);
129 textEl.appendChild(document.createTextNode(`: ${result}`));
130 li.appendChild(textEl);
131 if (r.guesses.length > 0) {
132 const emojiEl = document.createElement('div');
133 emojiEl.className = 'scoreboard__emoji';
134 emojiEl.textContent = guessesToEmoji(r.guesses, r.word);
135 li.appendChild(emojiEl);
136 }
137 ol.appendChild(li);
138 });
139
140 // Live round — show pending entry to the challenger only.
141 if (liveRoundIdx >= 0) {
142 const liveRound = rounds[liveRoundIdx];
143 if (liveRound && myRole === liveRound.challenger) {
144 const roundNum = liveRoundIdx + 1;
145 const solver: PlayerId = liveRound.challenger === 'a' ? 'b' : 'a';
146 const solverName = players[solver].name || (solver === 'a' ? 'Player A' : 'Player B');
147 const liveLi = document.createElement('li');
148 liveLi.className = 'scoreboard__round scoreboard__round--live';
149 const numSpan = document.createElement('span');
150 numSpan.className = 'scoreboard__round-num';
151 numSpan.textContent = `${roundNum}.`;
152 const nameSpan = document.createElement('span');
153 nameSpan.className = `scoreboard__name--${solver}`;
154 nameSpan.textContent = solverName;
155 liveLi.appendChild(numSpan);
156 liveLi.appendChild(nameSpan);
157 liveLi.appendChild(document.createTextNode(` is trying to solve ${liveRound.word}…`));
158 ol.appendChild(liveLi);
159 }
4a3e918a 2026-03-26 Pavel Safronov160 }
161
405e0a58 2026-03-27 Pavel Safronov162 historyEl.appendChild(ol);
163 } // end if historyVisible
bf4044de 2026-03-24 Pavel Safronov164 root.appendChild(historyEl);
165 }
166
167 render();
1c548168 2026-03-24 Pavel Safronov168 container.appendChild(root);
169
170 return function cleanup() {
171 container.removeChild(root);
172 };
173}