const { useState, useEffect, useRef } = React; const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); const ROW_LAYOUT = [6, 8, 8, 4]; const API_URL = "game_server.php"; const MOVES_PER_TURN = 4; const BLOCK_COLORS = ALPHABET.map((_, i) => [ "#E84B3A", "#F07A2A", "#F5C518", "#4CAF50", "#2196F3", "#9C27B0", "#00BCD4", "#FF5722", "#E91E63", "#607D8B", "#795548", "#009688", "#3F51B5", "#8BC34A", "#FF9800", "#F44336", "#673AB7", "#03A9F4", "#CDDC39", "#FFC107", "#76FF03", "#00E5FF", "#FF4081", "#AA00FF", "#FFAB40", "#69F0AE" ][i % 26]); const FALLEN_STYLES = ALPHABET.map((_, i) => { const rotation = (Math.random() - 0.5) * 70; return { rotate: rotation, spellingRotate: rotation * 0.4, dx: (Math.random() - 0.5) * 25, dy: (Math.random() - 0.5) * 15 + 5, scale: 0.85 + Math.random() * 0.15, }; }); function darken(hex) { if (!hex) return "#000"; const num = parseInt(hex.replace("#", ""), 16); const r = Math.max(0, ((num >> 16) & 0xff) - 50); const g = Math.max(0, ((num >> 8) & 0xff) - 50); const b = Math.max(0, (num & 0xff) - 50); return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`; } const playSound = (fileName) => { try { const audio = new Audio(`sounds/${fileName}`); audio.play().catch(() => { }); // catch and ignore if browser blocks autoplay } catch (e) { } }; // Cache the best available English voice once the browser loads its voice list let _bestVoice = null; function getBestVoice() { if (_bestVoice) return _bestVoice; const voices = window.speechSynthesis.getVoices(); if (!voices.length) return null; // Priority: Google US English > Google English > Microsoft (neural) > any en-US > any en const priority = [ v => v.name === 'Google US English', v => v.name.startsWith('Google') && v.lang.startsWith('en'), v => v.name.startsWith('Microsoft') && v.lang === 'en-US' && v.name.includes('Natural'), v => v.name.startsWith('Microsoft') && v.lang === 'en-US', v => v.lang === 'en-US', v => v.lang.startsWith('en'), ]; for (const test of priority) { const found = voices.find(test); if (found) { _bestVoice = found; return found; } } return null; } // Warm up the voice cache as soon as voices are ready if (window.speechSynthesis) { window.speechSynthesis.onvoiceschanged = () => { _bestVoice = null; getBestVoice(); }; getBestVoice(); } const playTTS = (text) => { if (!window.speechSynthesis) return; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = "en-US"; const voice = getBestVoice(); if (voice) utterance.voice = voice; window.speechSynthesis.speak(utterance); }; async function safeFetch(url) { try { const res = await fetch(url + "&_t=" + Date.now()); const contentType = res.headers.get("content-type"); if (!contentType || !contentType.includes("application/json")) return { status: "error" }; return await res.json(); } catch (err) { return { status: "error" }; } } function Cloud({ x, y, scale, speed }) { return (
{game.players.length > 1 ? `Get ready ${game.players[game.turn_index]?.name}, your turn is next!` : "Get ready, next word is coming..."}
{game.message}