document.addEventListener("DOMContentLoaded", () => { const roundDisplay = document.getElementById("round"); const streetDisplay = document.getElementById("street"); const potDisplay = document.getElementById("pot"); const communityCards = document.getElementById("community-cards"); const playerCards = document.getElementById("player-cards"); const spectatorNote = document.getElementById("spectator-note"); const playerName = document.getElementById("player-name"); const currentBet = document.getElementById("current-bet"); const yourContribution = document.getElementById("your-contribution"); const yourStack = document.getElementById("your-stack"); const currentTurn = document.getElementById("current-turn"); const turnState = document.getElementById("turn-state"); const turnQueue = document.getElementById("turn-queue"); const lastAction = document.getElementById("last-action"); const showdownPanel = document.getElementById("showdown-panel"); const errorMessage = document.getElementById("error-message"); const leaveTable = document.getElementById("leave-table"); const betModal = document.getElementById("bet-modal"); const closeBet = document.getElementById("close-bet"); const betRange = document.getElementById("bet-range"); const betValue = document.getElementById("bet-value"); const decreaseBet = document.getElementById("decrease-bet"); const increaseBet = document.getElementById("increase-bet"); const confirmBet = document.getElementById("confirm-bet"); const actionButtons = document.querySelectorAll(".actions button"); const params = new URLSearchParams(window.location.search); const lobbyId = params.get("lobbyId") || localStorage.getItem("activeLobbyId"); let socket = null; let socketReady = null; let isYourTurn = false; let canLeaveTable = true; let isSpectator = false; let currentStackValue = 400; let minimumStake = 10; let callAmount = 0; let countdownTimer = null; const actionTooltips = { hit: "Check or call if your bet already matches the table.", fold: "Give up this hand and lose your current stake.", bet: "Open the bet dialog to add chips to the pot." }; actionButtons.forEach((button) => { const tooltip = actionTooltips[button.dataset.action]; if (!tooltip) return; button.title = tooltip; button.setAttribute("aria-label", `${button.textContent.trim()}: ${tooltip}`); }); closeBet.title = "Close the bet dialog."; decreaseBet.title = "Decrease the bet amount."; increaseBet.title = "Increase the bet amount."; confirmBet.title = "Place the selected bet."; const showError = (message) => { errorMessage.textContent = message; errorMessage.classList.remove("hidden"); }; const hideError = () => { errorMessage.textContent = ""; errorMessage.classList.add("hidden"); }; const renderCard = (card, fallback) => { const cardElement = document.createElement("div"); cardElement.className = "card"; if (!card) { cardElement.classList.add("placeholder"); cardElement.textContent = fallback; return cardElement; } cardElement.classList.toggle("red", card.suit === "H" || card.suit === "D"); cardElement.innerHTML = `${card.rank}${card.suit}`; return cardElement; }; const renderCards = (container, cards, count, fallbackPrefix) => { container.innerHTML = ""; for (let index = 0; index < count; index++) { container.appendChild(renderCard(cards[index], `${fallbackPrefix} ${index + 1}`)); } }; const renderQueue = (queue = [], currentPlayer = null) => { turnQueue.innerHTML = ""; queue.forEach((player, index) => { const item = document.createElement("li"); item.textContent = index === 0 ? `${player} (now)` : player; item.classList.toggle("active", player === currentPlayer); turnQueue.appendChild(item); }); }; const renderShowdown = (showdown = [], winner = null) => { showdownPanel.innerHTML = ""; showdownPanel.classList.toggle("hidden", showdown.length === 0 && !winner); if (winner) { const heading = document.createElement("h2"); heading.textContent = `Winner: ${winner}`; showdownPanel.appendChild(heading); } showdown.forEach((entry) => { const row = document.createElement("div"); row.className = "showdown-row"; const name = document.createElement("span"); name.textContent = `${entry.player}: ${entry.result.name}`; row.appendChild(name); const cards = document.createElement("div"); cards.className = "mini-cards"; (entry.result.comboCards || entry.hand).forEach((card) => cards.appendChild(renderCard(card, "?"))); row.appendChild(cards); showdownPanel.appendChild(row); }); }; const sendSocketMessage = async (message) => { const ws = await connectSocket(); ws.send(JSON.stringify(message)); }; const updateActionButtons = () => { actionButtons.forEach((button) => { button.disabled = !isYourTurn || isSpectator; if (button.dataset.action === "hit") { button.title = callAmount > 0 ? `Call with $${Math.min(callAmount, currentStackValue)}.` : actionTooltips.hit; } }); confirmBet.disabled = !isYourTurn || isSpectator; leaveTable.disabled = false; leaveTable.title = "Leave table. Chips already in the pot are lost."; }; const stopCountdown = () => { if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } }; const renderIntermissionCountdown = (startsAt, baseMessage) => { stopCountdown(); const updateCountdown = () => { const seconds = Math.max(0, Math.ceil((Number(startsAt) - Date.now()) / 1000)); currentTurn.textContent = `${seconds}s`; turnState.textContent = "Waiting for next round"; lastAction.textContent = baseMessage || `Next round starts in ${seconds}s. Minimum stake: $${minimumStake}.`; if (seconds <= 0) { stopCountdown(); } }; updateCountdown(); countdownTimer = setInterval(updateCountdown, 1000); }; const renderTurnCountdown = (startsAt, currentPlayer) => { stopCountdown(); const updateCountdown = () => { const seconds = Math.max(0, Math.ceil((Number(startsAt) - Date.now()) / 1000)); currentTurn.textContent = `${currentPlayer || "Player"} (${seconds}s)`; turnState.textContent = isYourTurn ? `Your decision (${seconds}s)` : `Waiting for other player (${seconds}s)`; if (seconds <= 0) { stopCountdown(); } }; updateCountdown(); countdownTimer = setInterval(updateCountdown, 1000); }; const connectSocket = () => { if (socket && socket.readyState === WebSocket.OPEN) { return Promise.resolve(socket); } if (socketReady) { return socketReady; } const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; socket = new WebSocket(`${protocol}//${window.location.host}`); socketReady = new Promise((resolve, reject) => { socket.addEventListener("open", () => resolve(socket), {once: true}); socket.addEventListener("error", () => reject(new Error("WebSocket connection failed")), {once: true}); }); socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.type === "poker:state") { hideError(); isYourTurn = Boolean(message.isYourTurn); canLeaveTable = Boolean(message.canLeaveTable); isSpectator = Boolean(message.isSpectator); currentStackValue = Number(message.yourStack ?? 0); minimumStake = Number(message.minimumStake || 10); callAmount = Number(message.callAmount || 0); roundDisplay.textContent = message.round; potDisplay.textContent = message.pot; currentBet.textContent = message.currentBet ?? message.bet; yourContribution.textContent = message.yourContribution || 0; yourStack.textContent = currentStackValue; streetDisplay.textContent = message.street || "Preflop"; playerName.textContent = message.username; if (message.phase === "intermission" && message.nextRoundStartsAt) { renderIntermissionCountdown(message.nextRoundStartsAt, message.lastAction); } else if (message.phase === "betting" && message.turnEndsAt) { renderTurnCountdown(message.turnEndsAt, message.currentPlayer); lastAction.textContent = message.lastAction || ""; } else { stopCountdown(); currentTurn.textContent = message.currentPlayer || "Round ended"; turnState.textContent = isSpectator ? "Spectating" : isYourTurn ? "Your decision" : message.phase === "betting" ? "Waiting for other player" : message.phase; lastAction.textContent = message.lastAction || ""; } renderCards(communityCards, message.communityCards || [], Math.max(3, message.communityCards?.length || 0), "Dealer Card"); renderCards(playerCards, message.hand || [], isSpectator ? 0 : 2, "Card"); spectatorNote.classList.toggle("hidden", !isSpectator); renderQueue(message.queue || [], message.currentPlayer); renderShowdown(message.showdown || [], message.winner); updateActionButtons(); return; } if (message.type === "poker:action") { return; } if (message.type === "lobby:left") { localStorage.removeItem("activeLobbyId"); window.location.href = "/"; return; } if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") { showError(message.message || "Poker connection failed"); } }); socket.addEventListener("close", () => { socket = null; socketReady = null; }); return socketReady; }; const setBetValue = (value) => { const nextValue = Math.max(Number(betRange.min), Math.min(Number(betRange.max), Number(value))); betRange.value = nextValue; betValue.textContent = nextValue; }; const updateBetRange = () => { const maxBet = Math.max(0, currentStackValue); const requiredBet = Math.max(minimumStake, callAmount); const minBet = maxBet > 0 ? Math.min(requiredBet, maxBet) : 0; betRange.min = String(minBet); betRange.max = String(maxBet); betRange.step = String(minimumStake); setBetValue(Math.max(minBet, Number(betRange.value) || minBet)); }; if (!lobbyId) { showError("No active lobby found."); isYourTurn = false; updateActionButtons(); return; } updateActionButtons(); connectSocket() .then(() => sendSocketMessage({type: "poker:join", lobbyId})) .catch(() => showError("Could not connect to poker table.")); actionButtons.forEach((button) => { button.addEventListener("click", () => { const action = button.dataset.action; if (isSpectator) { showError("You are out of chips and can only spectate."); return; } if (!isYourTurn) { showError("Wait until it is your turn."); return; } if (action === "bet") { updateBetRange(); betModal.classList.remove("hidden"); return; } sendSocketMessage({type: "poker:action", action, payload: {lobbyId}}).catch(() => { showError("Could not send poker action."); }); }); }); leaveTable.addEventListener("click", () => { sendSocketMessage({type: "lobby:leave", lobbyId}).catch(() => { showError("Could not leave table."); }); }); closeBet.addEventListener("click", () => betModal.classList.add("hidden")); betRange.addEventListener("input", () => setBetValue(betRange.value)); decreaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) - 10)); increaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) + 10)); confirmBet.addEventListener("click", () => { if (isSpectator) { showError("You are out of chips and can only spectate."); betModal.classList.add("hidden"); return; } if (!isYourTurn) { showError("Wait until it is your turn."); betModal.classList.add("hidden"); return; } sendSocketMessage({ type: "poker:action", action: "bet", payload: {amount: Number(betRange.value), lobbyId} }).catch(() => showError("Could not place bet.")); betModal.classList.add("hidden"); }); });