-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
417 lines (375 loc) · 14.9 KB
/
script.js
File metadata and controls
417 lines (375 loc) · 14.9 KB
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
const sampleTexts = [
"The cricket match was in full swing as the afternoon sun cast long shadows across the perfectly manicured pitch. Spectators in white clothing dotted the pavilion, sipping tea and enjoying cucumber sandwiches.",
"On a glorious summer day at Lords, the players took their positions as the umpire called play. The batsman took his stance, ready to face the bowlers delivery in this quintessentially English sport.",
"The village green was alive with the sound of cricket as families gathered for the annual match. The pavilion clock chimed four as both teams fought for supremacy in this traditional English contest.",
"As the tea break approached, the score was delicately poised with the home team needing just twelve runs for victory. The afternoon sun filtered through the leaves of ancient elm trees."
];
let text = '';
let currentIndex = 0;
let startTime = null;
let timer = null;
let wrongChar = null; // Track if there's a wrong character at current position
let wordsTyped = 0;
let gameEnded = false;
let gameEndTimeout = null;
const GAME_DURATION_MS = 5 * 60 * 1000; // 5 minutes
let cricket = null;
let secondTickInterval = null;
let isGameRunning = false;
let selectedFlag = '';
let savedLineup = ['Tendulkar','Sehwag','Ganguly','Dravid','Kaif','Yuvraj','Pathan','Khan','Kumble','Harbhajan','Srisanth'];
selectedFlag = '🇮🇳';
let wormChart = null;
// External article loading
let preselectedText = '';
function normalizeToWordCount(input, desiredCount) {
const words = (input || '')
.replace(/\s+/g, ' ')
.trim()
.split(' ')
.filter(Boolean);
if (words.length === desiredCount) return words.join(' ');
if (words.length > desiredCount) return words.slice(0, desiredCount).join(' ');
// If shorter, repeat the content with a separator until we hit the desired count
const base = words.slice();
let i = 0;
while (words.length < desiredCount && base.length > 0) {
// insert a soft separator every ~120 words to feel article-like
if (words.length % 120 === 0) words.push('\u2014');
words.push(base[i % base.length]);
i++;
}
return words.slice(0, desiredCount).join(' ');
}
async function loadArticlesAndPickOne() {
try {
const res = await fetch('texts.json', { cache: 'no-store' });
if (!res.ok) throw new Error('Failed to load texts.json');
const data = await res.json();
const articles = Array.isArray(data) ? data : Array.isArray(data?.articles) ? data.articles : [];
const onlyStrings = articles.filter(a => typeof a === 'string');
if (onlyStrings.length === 0) throw new Error('No valid articles');
const idx = Math.floor(Math.random() * onlyStrings.length);
preselectedText = normalizeToWordCount(onlyStrings[idx], 1000);
} catch (e) {
// fallback handled by init()
preselectedText = '';
}
}
function buildThousandWordText(targetWordCount = 1000) {
const wordPools = sampleTexts.map(t => t.split(/\s+/));
const flatPool = [].concat(...wordPools);
const resultWords = [];
let i = 0;
while (resultWords.length < targetWordCount) {
resultWords.push(flatPool[i % flatPool.length]);
i++;
}
return resultWords.join(' ');
}
function init(customPlayers) {
text = preselectedText && preselectedText.length ? preselectedText : buildThousandWordText(1000);
currentIndex = 0;
startTime = null;
wrongChar = null;
wordsTyped = 0;
gameEnded = false;
if (timer) clearInterval(timer);
if (gameEndTimeout) clearTimeout(gameEndTimeout);
if (secondTickInterval) clearInterval(secondTickInterval);
if (!cricket) cricket = new CricketEngine({ onUpdate: onEngineUpdate, customPlayers });
else cricket.reset(customPlayers);
// Reset worm chart
if (!wormChart) wormChart = new StatsWormChart('wormChart');
wormChart.reset();
render();
updateStats();
}
function render() {
const display = document.getElementById('textDisplay');
let html = '';
for (let i = 0; i < text.length; i++) {
let className = 'char';
if (i < currentIndex) {
className += ' correct';
} else if (i === currentIndex) {
if (wrongChar !== null) {
className += ' incorrect';
} else {
className += ' current';
}
}
html += `<span class="${className}">${text[i]}</span>`;
}
display.innerHTML = html;
const currentChar = display.querySelector('.current') || display.querySelector('.char.incorrect');
if (currentChar) {
// Smooth scrolling to keep cursor centered
currentChar.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
function updateStats() {
if (!startTime) {
renderScoreboard();
renderScoreSummaryRight();
return;
}
const elapsed = (Date.now() - startTime) / 1000;
const wpm = Math.round((wordsTyped) / (elapsed / 60)) || 0;
const accuracy = currentIndex > 0 ? 100 : 100;
// Left-side WPM display removed; keep computation for future use if needed
if (cricket) {
renderScoreboard();
if (cricket.wickets >= 10) {
endGame();
}
renderScoreSummaryRight();
}
}
// UI handlers
function onStartInnings() {
if (savedLineup.length !== 11) return;
startGame(savedLineup);
}
function onDeclare() {
if (isGameRunning) endGame(true);
}
document.addEventListener('keydown', function(e) {
// Allow starting the innings by typing a character if not already running
if (!isGameRunning && !gameEnded && e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
onStartInnings();
}
if (!isGameRunning || gameEnded || currentIndex >= text.length) return;
if (e.key === 'Backspace') {
// Backspace disabled (like keybr)
e.preventDefault();
return;
}
if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
const expectedChar = text[currentIndex];
if (e.key === expectedChar) {
currentIndex++;
wrongChar = null;
// Word completed when we type a space or reach text end
if (expectedChar === ' ' || currentIndex === text.length) {
wordsTyped += 1;
if (cricket) cricket.onWord();
}
} else {
wrongChar = e.key;
if (cricket) cricket.onError();
}
render();
updateStats();
if (currentIndex >= text.length) {
endGame(true);
}
}
});
document.addEventListener('click', (e) => {
// Do not steal focus from inputs or while modal is open
const isInModal = !!(e.target && e.target.closest && e.target.closest('.modal'));
const isInteractive = !!(e.target && e.target.closest && e.target.closest('input, textarea, select, button'));
if (isInModal || isInteractive) return;
if (isGameRunning) {
document.body.focus();
}
});
document.body.setAttribute('tabindex', '0');
// initial render and engine init with default lineup
window.addEventListener('DOMContentLoaded', () => {
// Try to load long-form articles, then init
loadArticlesAndPickOne().finally(() => {
init(savedLineup);
renderScoreSummaryRight();
render();
updateStats();
const startBtn = document.getElementById('startInningsBtn');
if (startBtn) startBtn.disabled = false;
const flagEl = document.getElementById('teamFlag');
if (flagEl) flagEl.textContent = selectedFlag;
});
});
function onEngineUpdate(snapshot) {
renderScoreboard();
if (wormChart) wormChart.update({
totalRuns: cricket.totalRuns,
wickets: cricket.wickets,
overs: cricket.overs,
balls: cricket.balls,
players: cricket.players
});
renderScoreSummaryRight();
}
function renderScoreboard() {
const el = document.getElementById('cricketScoreboard');
if (!el || !cricket) return;
el.innerHTML = cricket.buildScoreboardHTML();
enableInlineNameEditing(el);
}
function renderScoreSummaryRight() {
const el = document.getElementById('scoreSummaryRight');
if (!el || !cricket) return;
// Compute run rate by time-based method as engine does
let elapsed = 0;
if (startTime) elapsed = (Date.now() - startTime) / 1000;
const rr = cricket.getRunRate(elapsed).toFixed(2);
el.textContent = `${selectedFlag ? selectedFlag + ' ' : ''}${cricket.totalRuns}/${cricket.wickets} ${cricket.overs}.${cricket.balls} overs • RR ${rr}`;
}
function enableInlineNameEditing(root) {
// Make batter names clickable and editable
const rows = root.querySelectorAll('tbody tr');
rows.forEach((row, idx) => {
const nameCell = row.children[0];
if (!nameCell) return;
nameCell.style.cursor = 'text';
nameCell.title = 'Click to edit name';
nameCell.addEventListener('click', () => {
const currentName = cricket.players[idx].name;
const input = document.createElement('input');
input.type = 'text';
input.value = currentName;
input.style.width = '90%';
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') input.blur();
if (e.key === 'Escape') {
input.value = currentName;
input.blur();
}
e.stopPropagation();
});
input.addEventListener('click', (e) => e.stopPropagation());
input.addEventListener('blur', () => {
const newName = input.value.trim() || currentName;
cricket.players[idx].name = newName;
renderScoreboard();
});
nameCell.textContent = '';
nameCell.appendChild(input);
input.focus();
input.select();
});
});
}
function endGame(fromCompletion = false) {
if (gameEnded) return;
gameEnded = true;
if (timer) clearInterval(timer);
if (gameEndTimeout) clearTimeout(gameEndTimeout);
if (secondTickInterval) clearInterval(secondTickInterval);
const display = document.getElementById('textDisplay');
const msg = fromCompletion ? '🏏 Well played! Innings complete!' : '🏏 Time! Innings complete!';
display.innerHTML += `<div class="completed">${msg}</div>`;
isGameRunning = false;
const startBtn = document.getElementById('startInningsBtn');
const declareBtn = document.getElementById('declareBtn');
if (startBtn) startBtn.style.display = '';
if (declareBtn) declareBtn.style.display = 'none';
}
// Lineup modal and saving lineup
function openLineupModal(forceShow = false) {
const modal = document.getElementById('setupModal');
const saveBtn = document.getElementById('setupSaveBtn');
const cancelBtn = document.getElementById('setupCancelBtn');
const playersInput = document.getElementById('playerNamesInput');
const flagSelect = document.getElementById('teamFlagSelect');
const playerCount = document.getElementById('playerCount');
const startBtn = document.getElementById('startInningsBtn');
const errorEl = document.getElementById('setupError');
if (!modal || !saveBtn || !cancelBtn) return;
modal.classList.remove('hidden');
// Prevent body focus stealing while modal is open
document.removeEventListener('click', bodyFocusClickOnce, true);
// Seed existing values
playersInput.value = (savedLineup && savedLineup.length) ? savedLineup.join('\n') : '';
flagSelect.value = selectedFlag || '';
errorEl.style.display = 'none';
function updateCountAndButton() {
const names = (playersInput.value || '').split(/\n+/).map(s => s.trim()).filter(Boolean);
playerCount.textContent = `Players: ${names.length}/10`;
const valid = names.length === 10;
startBtn.disabled = !valid;
return { names, valid };
}
updateCountAndButton();
playersInput.addEventListener('input', updateCountAndButton);
const saveHandler = () => {
const { names, valid } = updateCountAndButton();
if (!valid) {
errorEl.textContent = 'Please enter exactly 10 player names.';
errorEl.style.display = 'block';
return;
}
savedLineup = names;
selectedFlag = flagSelect.value || '';
document.getElementById('teamFlag').textContent = selectedFlag;
modal.classList.add('hidden');
// Reattach focus handler after modal closes
document.addEventListener('click', bodyFocusClickOnce, true);
cleanup();
};
const cancelHandler = () => {
modal.classList.add('hidden');
// Reattach focus handler after modal closes
document.addEventListener('click', bodyFocusClickOnce, true);
cleanup();
};
function cleanup() {
saveBtn.removeEventListener('click', saveHandler);
cancelBtn.removeEventListener('click', cancelHandler);
playersInput.removeEventListener('input', updateCountAndButton);
}
saveBtn.addEventListener('click', saveHandler);
cancelBtn.addEventListener('click', cancelHandler);
}
// Encapsulate body focus logic so we can add/remove cleanly
function bodyFocusClickOnce(e) {
const isInModal = !!(e.target && e.target.closest && e.target.closest('.modal'));
const isInteractive = !!(e.target && e.target.closest && e.target.closest('input, textarea, select, button'));
if (isInModal || isInteractive) return;
if (isGameRunning) {
document.body.focus();
}
}
function startGameWithDefaults() {
startGame(null);
}
function startGame(customPlayers) {
init(customPlayers);
isGameRunning = true;
gameEnded = false;
currentIndex = 0;
startTime = Date.now();
wrongChar = null;
const startBtn = document.getElementById('startInningsBtn');
const declareBtn = document.getElementById('declareBtn');
if (startBtn) startBtn.style.display = 'none';
if (declareBtn) declareBtn.style.display = '';
// Start timers immediately on innings start
if (timer) clearInterval(timer);
if (gameEndTimeout) clearTimeout(gameEndTimeout);
if (secondTickInterval) clearInterval(secondTickInterval);
timer = setInterval(updateStats, 100);
// Align first tick to the next second boundary; stop exactly at 300 balls
const tick = () => {
if (!cricket) return;
cricket.onSecondTick();
const totalBalls = cricket.overs * 6 + cricket.balls;
if (isGameRunning && totalBalls >= 300) {
endGame();
}
};
const now = Date.now();
const msToNextSecond = 1000 - (now % 1000);
setTimeout(() => {
tick();
secondTickInterval = setInterval(tick, 1000);
}, msToNextSecond);
// focus to capture keystrokes
document.body.focus();
// immediate UI refresh
renderScoreboard();
updateStats();
renderScoreSummaryRight();
}