try to fix this spaghetti of a code

This commit is contained in:
Frederik Spirgi
2026-09-09 10:48:55 +02:00
parent 9e099a41db
commit a5f8191c2e
13 changed files with 306 additions and 2318 deletions
+36 -1600
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -27,6 +27,6 @@
"ws": "^8.20.0" "ws": "^8.20.0"
}, },
"devDependencies": { "devDependencies": {
"swagger-autogen": "^2.23.7" "@playwright/test": "^1.63.0"
} }
} }
+59 -373
View File
@@ -1,382 +1,68 @@
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener('DOMContentLoaded', async () => {
const logoutButton = document.getElementById("logout"); const byId = id => document.getElementById(id);
const balance = document.getElementById("balance"); const client = new RealtimeClient();
const hostButton = document.getElementById("host"); let username; let lobbyId;
const joinButton = document.getElementById("join"); const error = message => {byId('join-error').textContent = message; byId('join-error').classList.toggle('hidden', !message);};
const hostForm = document.getElementById("host-form"); const send = async message => {try {error(''); await client.send(message);} catch (problem) {error(problem.message);}};
const createLobbyButton = document.getElementById("create-lobby");
const lobbyVisibilityInputs = document.querySelectorAll("input[name='lobby-visibility']");
const joinForm = document.getElementById("join-form");
const joinCodeInput = document.getElementById("join-code");
const joinSubmit = document.getElementById("submit-join");
const refreshLobbies = document.getElementById("refresh-lobbies");
const publicLobbies = document.getElementById("public-lobbies");
const leaveButton = document.getElementById("leave-button");
const joinError = document.getElementById("join-error");
const lobbyRoom = document.getElementById("lobby-room");
const lobbyIdDisplay = document.getElementById("lobby-id");
const playerList = document.getElementById("player-list");
const startGameButton = document.getElementById("start-game");
let socket = null;
let socketReady = null;
let currentUsername = null;
let currentLobbyId = null;
const showJoinError = (message) => {
joinError.textContent = message;
joinError.classList.remove("hidden");
};
const clearJoinError = () => {
joinError.textContent = "";
joinError.classList.add("hidden");
};
const renderPlayers = (players) => {
playerList.innerHTML = "";
if (!players.length) {
playerList.innerHTML = "<p>No players yet</p>";
return;
}
for (const player of players) {
const entry = document.createElement("p");
entry.textContent = player;
playerList.appendChild(entry);
}
};
const updateStartButton = (host, canStart) => {
const isHost = currentUsername && host === currentUsername;
startGameButton.classList.toggle("hidden", !isHost);
startGameButton.disabled = !isHost || !canStart;
startGameButton.textContent = canStart ? "Start Game" : "Waiting for 2 players";
};
const showLobby = (lobbyId, players = [], host = null, canStart = false) => {
currentLobbyId = lobbyId;
lobbyIdDisplay.textContent = lobbyId;
lobbyRoom.classList.remove("hidden");
joinForm.classList.add("hidden");
hostForm.classList.add("hidden");
renderPlayers(players);
updateStartButton(host, canStart);
};
const joinLobby = (lobbyId) => {
if (!currentUsername) {
showJoinError("User not authenticated.");
return Promise.reject(new Error("User not authenticated"));
}
return sendSocketMessage({type: "lobby:join", lobbyId});
};
const renderPublicLobbies = (lobbies = []) => {
publicLobbies.innerHTML = "";
if (!lobbies.length) {
publicLobbies.innerHTML = "<p>No public lobbies found</p>";
return;
}
lobbies.forEach((lobby) => {
const row = document.createElement("div");
row.className = "public-lobby";
const info = document.createElement("div");
info.className = "public-lobby-info";
const title = document.createElement("strong");
title.textContent = lobby.host ? `${lobby.host}'s lobby` : "Public lobby";
info.appendChild(title);
const meta = document.createElement("span");
meta.textContent = `${lobby.players} player${lobby.players === 1 ? "" : "s"} - ${lobby.gameStarted ? "Game running" : "Waiting"}`;
info.appendChild(meta);
const join = document.createElement("button");
join.type = "button";
join.textContent = "Join";
join.addEventListener("click", () => {
clearJoinError();
joinLobby(lobby.lobbyId).catch(() => showJoinError("Could not join lobby."));
});
row.appendChild(info);
row.appendChild(join);
publicLobbies.appendChild(row);
});
};
const loadPublicLobbies = async () => {
try {
publicLobbies.innerHTML = "<p>Loading lobbies...</p>";
const res = await fetch("/lobby/public", {credentials: "same-origin"});
if (!res.ok) {
throw new Error("Failed to load lobbies");
}
const data = await res.json();
renderPublicLobbies(data.lobbies || []);
} catch (error) {
publicLobbies.innerHTML = "<p>Could not load lobbies</p>";
}
};
const hideLobby = () => { const hideLobby = () => {
currentLobbyId = null; lobbyId = null; localStorage.removeItem('activeLobbyId'); byId('lobby-room').classList.add('hidden');
lobbyRoom.classList.add("hidden"); byId('host').disabled = false; byId('join').disabled = false;
lobbyIdDisplay.textContent = "N/A";
playerList.innerHTML = "<p>Waiting for players...</p>";
startGameButton.classList.add("hidden");
startGameButton.disabled = true;
}; };
async function refreshBalance() {
const sendSocketMessage = async (message) => {
const ws = await connectSocket();
ws.send(JSON.stringify(message));
};
const connectSocket = () => {
if (socket && socket.readyState === WebSocket.OPEN && socket.authenticated) {
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("error", () => reject(new Error("WebSocket connection failed")), {once: true});
socket.addEventListener("close", (event) => {
reject(new Error(`WebSocket closed before authentication: ${event.reason || event.code}`));
}, {once: true});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.type === "auth:success") {
currentUsername = message.username;
socket.authenticated = true;
resolve(socket);
}
if (message.type === "auth:error") {
reject(new Error(message.message || "WebSocket authentication failed"));
}
}, {once: true});
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.type === "auth:error") {
showJoinError(message.message);
return;
}
if (message.type === "auth:success") {
currentUsername = message.username;
return;
}
if (message.type === "lobby:state") {
clearJoinError();
if (message.gameStarted) {
localStorage.setItem("activeLobbyId", message.lobbyId);
window.location.href = message.gameUrl || `/poker.html?lobbyId=${encodeURIComponent(message.lobbyId)}`;
return;
}
showLobby(message.lobbyId, message.players || [], message.host, Boolean(message.canStart));
return;
}
if (message.type === "poker:started") {
localStorage.setItem("activeLobbyId", message.lobbyId);
window.location.href = message.url || `/poker.html?lobbyId=${encodeURIComponent(message.lobbyId)}`;
return;
}
if (message.type === "lobby:left") {
hideLobby();
return;
}
if (message.type === "lobby:error" || message.type === "error" || message.type === "poker:error") {
showJoinError(message.message || "WebSocket request failed");
}
});
socket.addEventListener("close", () => {
socket = null;
socketReady = null;
});
return socketReady;
};
const getUsernameFromToken = async () => {
try { try {
const res = await fetch("/verify", { const [balance, ranks] = await Promise.all([apiRequest('/balance'), apiRequest('/leaderboard')]);
method: "POST", byId('balance').textContent = balance.balance;
credentials: "same-origin" byId('leaderboard-list').replaceChildren(...ranks.leaderboard.map((player, index) => {
}); const item = document.createElement('li'); item.textContent = `${index + 1}. ${player.username}: ${player.money}`;
if (!res.ok) { item.classList.toggle('you', player.username === username); return item;
console.error("Token verification failed", res.status, await res.text()); }));
return null; byId('your-rank').textContent = `Your rank: #${ranks.self.position}`;
} } catch (problem) {error(problem.message);}
const data = await res.json();
return data.user?.username;
} catch (err) {
console.error("Failed to get username from token", err);
return null;
}
};
currentUsername = await getUsernameFromToken();
if (!currentUsername) {
showJoinError("User not authenticated.");
} }
client.addEventListener('status', event => {error(event.detail); byId('start-game').disabled = !client.ready || byId('start-game').dataset.allowed !== 'true';});
connectSocket().catch((error) => { client.addEventListener('message', event => {
console.error("Home WebSocket connection failed:", error); const message = event.detail;
showJoinError("Real-time poker connection failed."); if (message.type === 'auth:success') username = message.username;
if (message.type === 'lobby:left') {hideLobby(); refreshBalance();}
if (message.type !== 'lobby:state') return;
lobbyId = message.lobbyId;
if (message.gameStarted) {
localStorage.setItem('activeLobbyId', lobbyId);
location.assign(`/poker.html?lobbyId=${encodeURIComponent(lobbyId)}`); return;
}
byId('lobby-id').textContent = lobbyId; byId('lobby-room').classList.remove('hidden');
byId('host-form').classList.add('hidden'); byId('join-form').classList.add('hidden');
byId('host').disabled = true; byId('join').disabled = true;
byId('player-list').replaceChildren(...message.players.map(player => {const item = document.createElement('p'); item.textContent = player; return item;}));
const allowed = message.host === username && message.canStart;
byId('start-game').dataset.allowed = String(allowed);
byId('start-game').disabled = !allowed; byId('start-game').classList.toggle('hidden', message.host !== username);
}); });
byId('host').addEventListener('click', () => {byId('host-form').classList.toggle('hidden'); byId('join-form').classList.add('hidden');});
try { byId('create-lobby').addEventListener('click', () => send({type: 'lobby:create', private: document.querySelector('[name="lobby-visibility"]:checked').value === 'private'}));
const balanceRes = await fetch("/balance", { async function loadLobbies() {
method: "GET",
credentials: "same-origin"
});
if (balanceRes.ok) {
const balanceData = await balanceRes.json();
balance.textContent = balanceData.balance;
}
} catch (error) {
console.log("Balance error:", error);
}
logoutButton.addEventListener("click", async (e) => {
e.preventDefault();
try { try {
if (socket && socket.readyState === WebSocket.OPEN && socket.authenticated) { const {lobbies} = await apiRequest('/lobby/public');
socket.close(); byId('public-lobbies').replaceChildren();
if (!lobbies.length) {byId('public-lobbies').textContent = 'No public lobbies found'; return;}
for (const lobby of lobbies) {
const row = document.createElement('div'); row.className = 'public-lobby';
const info = document.createElement('span'); info.textContent = `${lobby.host}: ${lobby.players}/10 players`;
const button = document.createElement('button'); button.textContent = 'Join'; button.type = 'button';
button.addEventListener('click', () => send({type: 'lobby:join', lobbyId: lobby.lobbyId}));
row.append(info, button); byId('public-lobbies').append(row);
} }
const logoutRes = await fetch("/logout", { } catch (problem) {error(problem.message);}
method: "DELETE",
credentials: "same-origin"
});
if (logoutRes.ok) {
localStorage.removeItem("activeLobbyId");
window.location.href = "/login.html";
}
} catch (err) {
console.log("Logout error:", err);
}
});
hostButton.addEventListener("click", async (e) => {
e.preventDefault();
clearJoinError();
hostForm.classList.toggle("hidden");
joinForm.classList.add("hidden");
});
createLobbyButton.addEventListener("click", async (e) => {
e.preventDefault();
clearJoinError();
if (!currentUsername) {
showJoinError("User not authenticated.");
return;
}
const selectedVisibility = [...lobbyVisibilityInputs].find(input => input.checked)?.value || "public";
sendSocketMessage({type: "lobby:create", private: selectedVisibility === "private"}).catch(() => {
showJoinError("Could not create lobby.");
});
});
joinButton.addEventListener("click", (e) => {
e.preventDefault();
joinForm.classList.toggle("hidden");
hostForm.classList.add("hidden");
if (!joinForm.classList.contains("hidden")) {
joinCodeInput.focus();
loadPublicLobbies();
}
});
refreshLobbies.addEventListener("click", (e) => {
e.preventDefault();
loadPublicLobbies();
});
joinSubmit.addEventListener("click", async (e) => {
e.preventDefault();
clearJoinError();
const lobbyId = joinCodeInput.value.trim();
if (!lobbyId) {
showJoinError("Please enter a lobby ID.");
return;
}
if (!currentUsername) {
showJoinError("User not authenticated.");
return;
}
joinLobby(lobbyId).then(() => {
joinForm.classList.add("hidden");
joinCodeInput.value = "";
}).catch(() => {
showJoinError("Could not join lobby. Please try again.");
});
});
leaveButton.addEventListener("click", (e) => {
e.preventDefault();
const lobbyId = lobbyIdDisplay.textContent;
if (!lobbyId || lobbyId === "N/A") return;
sendSocketMessage({type: "lobby:leave", lobbyId}).catch(() => {
showJoinError("Could not leave lobby.");
});
});
startGameButton.addEventListener("click", (e) => {
e.preventDefault();
if (!currentLobbyId || startGameButton.disabled) return;
sendSocketMessage({type: "poker:start", lobbyId: currentLobbyId}).catch(() => {
showJoinError("Could not start game.");
});
});
async function loadLeaderboard() {
try {
const res = await fetch("/leaderboard", {credentials: "same-origin"});
if (res.ok) {
const data = await res.json();
const list = document.getElementById("leaderboard-list");
const yourRank = document.getElementById("your-rank");
list.innerHTML = "";
data.leaderboard.forEach((player, i) => {
const item = document.createElement("li");
item.textContent = `${i + 1}. ${player.username} - $${player.money}`;
if (player.username === data.self.username) {
item.classList.add("you");
item.textContent += " (you)";
}
list.appendChild(item);
});
yourRank.textContent = `You are ranked #${data.self.position} with $${data.self.money}`;
} else {
console.error("Failed to load leaderboard.");
}
} catch (err) {
console.error("Leaderboard error:", err);
}
} }
byId('join').addEventListener('click', () => {byId('join-form').classList.toggle('hidden'); byId('host-form').classList.add('hidden'); byId('join-code').focus(); loadLobbies();});
await loadLeaderboard(); byId('refresh-lobbies').addEventListener('click', loadLobbies);
byId('submit-join').addEventListener('click', () => send({type: 'lobby:join', lobbyId: byId('join-code').value.trim().toLowerCase()}));
byId('leave-button').addEventListener('click', () => send({type: 'lobby:leave', lobbyId}));
byId('start-game').addEventListener('click', () => send({type: 'poker:start', lobbyId}));
byId('logout').addEventListener('click', async () => {
try {await apiRequest('/logout', {method: 'DELETE'}); client.close(); localStorage.removeItem('activeLobbyId'); location.assign('/login');}
catch (problem) {error(`Logout failed: ${problem.message}`);}
});
try {username = (await apiRequest('/verify', {method: 'POST'})).user.username; await client.connect(); await refreshBalance();}
catch (problem) {error(problem.message);}
}); });
+17 -106
View File
@@ -1,109 +1,20 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector("form"); const form = document.querySelector('form');
const registerButton = document.getElementById("register-button"); const error = document.getElementById('auth-error');
const loginButton = document.getElementById("login-button"); let pending = false;
const authError = document.getElementById("auth-error"); async function authenticate(endpoint) {
if (pending || !form.reportValidity()) return;
form.addEventListener("submit", async (event) => { pending = true; error.textContent = ''; error.classList.add('hidden');
event.preventDefault(); for (const button of form.querySelectorAll('button')) button.disabled = true;
await authenticate("/login", "Login successful. Loading Lobby...");
});
registerButton.addEventListener("click", async () => {
await authenticate("/register", "Register successful. Loading Lobby...");
});
async function authenticate(endpoint, successMessage) {
clearAuthError();
setButtonsDisabled(true);
const credentials = {
username: form.username.value,
password: form.password.value
};
try { try {
const response = await fetch(endpoint, { const response = await fetch(endpoint, {method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'},
method: "POST", body: JSON.stringify({username: form.username.value, password: form.password.value})});
headers: {"Content-Type": "application/json"}, const data = await response.json();
credentials: "same-origin", if (!response.ok) throw new Error(data.error || 'Authentication failed');
body: JSON.stringify(credentials) location.assign('/');
}); } catch (problem) {error.textContent = problem.message; error.classList.remove('hidden');}
finally {pending = false; for (const button of form.querySelectorAll('button')) button.disabled = false;}
const data = await readJsonResponse(response);
if (!response.ok) {
showAuthError(data.error || "Authentication failed.");
return;
}
const verified = await verifySession();
if (!verified) {
showAuthError("Login succeeded, but the browser did not send the session cookie back to the server.");
return;
}
showLoadingScreen(successMessage);
} catch (error) {
console.error("Authentication request failed:", error);
showAuthError("Server unreachable.");
} finally {
setButtonsDisabled(false);
}
}
async function verifySession() {
const response = await fetch("/verify", {
method: "POST",
credentials: "same-origin"
});
if (!response.ok) return false;
const data = await readJsonResponse(response);
return Boolean(data.valid && data.user?.username);
}
async function readJsonResponse(response) {
try {
return await response.json();
} catch {
return {};
}
}
function showAuthError(message) {
authError.textContent = message;
authError.classList.remove("hidden");
}
function clearAuthError() {
authError.textContent = "";
authError.classList.add("hidden");
}
function setButtonsDisabled(disabled) {
loginButton.disabled = disabled;
registerButton.disabled = disabled;
}
function showLoadingScreen(message) {
const loadingScreen = document.getElementById("loading-screen");
const loadingText = document.getElementById("loading-text");
const progress = document.getElementById("progress");
loadingText.textContent = message;
loadingScreen.classList.remove("hidden");
let percent = 0;
const interval = setInterval(() => {
percent += Math.floor(Math.random() * 10) + 5;
if (percent >= 100) {
percent = 100;
progress.style.width = percent + "%";
clearInterval(interval);
setTimeout(() => {
window.location.href = "/";
}, 500);
} else {
progress.style.width = percent + "%";
}
}, 200);
} }
form.addEventListener('submit', event => {event.preventDefault(); authenticate('/login');});
document.getElementById('register-button').addEventListener('click', () => authenticate('/register'));
}); });
+50 -57
View File
@@ -26,8 +26,11 @@ document.addEventListener("DOMContentLoaded", () => {
const actionButtons = document.querySelectorAll(".actions button"); const actionButtons = document.querySelectorAll(".actions button");
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const lobbyId = params.get("lobbyId") || localStorage.getItem("activeLobbyId"); const lobbyId = params.get("lobbyId") || localStorage.getItem("activeLobbyId");
let socket = null; const client = new RealtimeClient();
let socketReady = null; let minimumRaise = 10;
let raisesAllowed = true;
let actionPending = false;
let previousFocus = null;
let isYourTurn = false; let isYourTurn = false;
let isSpectator = false; let isSpectator = false;
let currentStackValue = 0; let currentStackValue = 0;
@@ -120,13 +123,20 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
const sendSocketMessage = async (message) => { const sendSocketMessage = async (message) => {
const ws = await connectSocket(); message = {...message, lobbyId};
ws.send(JSON.stringify(message)); if (message.payload?.amount !== undefined) message.payload = {amount: message.payload.amount};
else delete message.payload;
actionPending = true; updateActionButtons();
try { await client.send(message); }
finally { actionPending = false; updateActionButtons(); }
};
const closeModal = () => {
betModal.classList.add("hidden");
previousFocus?.focus();
}; };
const updateActionButtons = () => { const updateActionButtons = () => {
actionButtons.forEach((button) => { actionButtons.forEach((button) => {
button.disabled = !isYourTurn || isSpectator; button.disabled = !client.ready || actionPending || !isYourTurn || isSpectator;
if (button.dataset.action === "hit") { if (button.dataset.action === "hit") {
button.textContent = callAmount > 0 ? "Call" : "Check"; button.textContent = callAmount > 0 ? "Call" : "Check";
button.title = callAmount > 0 button.title = callAmount > 0
@@ -134,9 +144,9 @@ document.addEventListener("DOMContentLoaded", () => {
: actionTooltips.hit; : actionTooltips.hit;
} }
}); });
confirmBet.disabled = !isYourTurn || isSpectator; confirmBet.disabled = !client.ready || actionPending || !isYourTurn || isSpectator;
leaveTable.disabled = false; leaveTable.disabled = !client.ready || actionPending;
leaveTable.title = "Leave table. Chips already in the pot are lost."; leaveTable.title = "Leave and fold. Your committed chips stay in this hand until settlement.";
}; };
const stopCountdown = () => { const stopCountdown = () => {
@@ -177,35 +187,13 @@ document.addEventListener("DOMContentLoaded", () => {
countdownTimer = setInterval(updateCountdown, 1000); countdownTimer = setInterval(updateCountdown, 1000);
}; };
const connectSocket = () => { client.addEventListener('status', event => {
if (socket && socket.readyState === WebSocket.OPEN && socket.authenticated) { if (event.detail) showError(event.detail);
return Promise.resolve(socket); if (!client.ready) {isYourTurn = false; stopCountdown(); closeModal();}
} updateActionButtons();
});
if (socketReady) { client.addEventListener('message', event => {
return socketReady; const message = event.detail;
}
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
socket = new WebSocket(`${protocol}//${window.location.host}`);
socketReady = new Promise((resolve, reject) => {
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.type === "auth:success") {
socket.authenticated = true;
resolve(socket);
} else {
reject(new Error(message.message || "Authentication failed"));
}
}, {once: true});
socket.addEventListener("close", () => reject(new Error("Connection closed")), {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") { if (message.type === "poker:state") {
hideError(); hideError();
isYourTurn = Boolean(message.isYourTurn); isYourTurn = Boolean(message.isYourTurn);
@@ -213,6 +201,10 @@ document.addEventListener("DOMContentLoaded", () => {
currentStackValue = Number(message.yourStack ?? 0); currentStackValue = Number(message.yourStack ?? 0);
minimumStake = Number(message.minimumStake || 10); minimumStake = Number(message.minimumStake || 10);
callAmount = Number(message.callAmount || 0); callAmount = Number(message.callAmount || 0);
minimumRaise = message.minimumRaise || minimumStake;
raisesAllowed = message.canRaise !== false;
if (!isYourTurn || isSpectator) closeModal();
else if (!betModal.classList.contains("hidden")) updateBetRange();
roundDisplay.textContent = message.round; roundDisplay.textContent = message.round;
potDisplay.textContent = message.pot; potDisplay.textContent = message.pot;
currentBet.textContent = message.currentBet ?? message.bet; currentBet.textContent = message.currentBet ?? message.bet;
@@ -257,19 +249,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") { if (message.type === "auth:error" || message.type === "poker:error" || message.type === "error") {
showError(message.message || "Poker connection failed"); showError(message.message || "Poker connection failed");
} }
}); });
socket.addEventListener("close", () => {
isYourTurn = false;
stopCountdown();
updateActionButtons();
showError("Connection lost. Reload to reconnect to the table.");
socket = null;
socketReady = null;
});
return socketReady;
};
const setBetValue = (value) => { const setBetValue = (value) => {
const nextValue = Math.max(Number(betRange.min), Math.min(Number(betRange.max), Number(value))); const nextValue = Math.max(Number(betRange.min), Math.min(Number(betRange.max), Number(value)));
@@ -278,12 +258,14 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
const updateBetRange = () => { const updateBetRange = () => {
const maxBet = Math.max(0, currentStackValue); const maxBet = Math.max(0, raisesAllowed ? currentStackValue : Math.min(currentStackValue, callAmount));
const requiredBet = Math.max(minimumStake, callAmount); const requiredBet = raisesAllowed ? callAmount + minimumRaise : callAmount;
const minBet = maxBet > 0 ? Math.min(requiredBet, maxBet) : 0; const minBet = maxBet > 0 ? Math.min(requiredBet, maxBet) : 0;
betRange.min = String(minBet); betRange.min = String(minBet);
betRange.max = String(maxBet); betRange.max = String(maxBet);
betRange.step = "1"; betRange.step = "1";
document.getElementById("range-min").textContent = `$${minBet}`;
document.getElementById("range-max").textContent = `$${maxBet}`;
setBetValue(Math.max(minBet, Number(betRange.value) || minBet)); setBetValue(Math.max(minBet, Number(betRange.value) || minBet));
}; };
@@ -296,8 +278,8 @@ document.addEventListener("DOMContentLoaded", () => {
updateActionButtons(); updateActionButtons();
connectSocket() client.connect()
.then(() => sendSocketMessage({type: "poker:join", lobbyId})) .then(() => client.send({type: "poker:join", lobbyId}))
.catch(() => showError("Could not connect to poker table.")); .catch(() => showError("Could not connect to poker table."));
actionButtons.forEach((button) => { actionButtons.forEach((button) => {
@@ -313,7 +295,9 @@ document.addEventListener("DOMContentLoaded", () => {
} }
if (action === "bet") { if (action === "bet") {
updateBetRange(); updateBetRange();
previousFocus = document.activeElement;
betModal.classList.remove("hidden"); betModal.classList.remove("hidden");
betRange.focus();
return; return;
} }
@@ -329,7 +313,16 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}); });
closeBet.addEventListener("click", () => betModal.classList.add("hidden")); closeBet.addEventListener("click", closeModal);
betModal.addEventListener("keydown", event => {
if (event.key === "Escape") {event.preventDefault(); closeModal();}
if (event.key === "Tab") {
const items = [...betModal.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
const first = items[0], last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {event.preventDefault(); last.focus();}
else if (!event.shiftKey && document.activeElement === last) {event.preventDefault(); first.focus();}
}
});
betRange.addEventListener("input", () => setBetValue(betRange.value)); betRange.addEventListener("input", () => setBetValue(betRange.value));
decreaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) - 10)); decreaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) - 10));
increaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) + 10)); increaseBet.addEventListener("click", () => setBetValue(Number(betRange.value) + 10));
@@ -349,6 +342,6 @@ document.addEventListener("DOMContentLoaded", () => {
action: "bet", action: "bet",
payload: {amount: Number(betRange.value), lobbyId} payload: {amount: Number(betRange.value), lobbyId}
}).catch(() => showError("Could not place bet.")); }).catch(() => showError("Could not place bet."));
betModal.classList.add("hidden"); closeModal();
}); });
}); });
+5 -1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home</title> <title>Home</title>
<link rel="stylesheet" href="shared.css">
<link rel="stylesheet" href="styles_home.css"> <link rel="stylesheet" href="styles_home.css">
<link rel="icon" href="gambling.png" type="image/x-icon"> <link rel="icon" href="gambling.png" type="image/x-icon">
</head> </head>
@@ -41,6 +42,7 @@
</div> </div>
<div id="join-form" class="join-browser hidden"> <div id="join-form" class="join-browser hidden">
<div class="join-code-row"> <div class="join-code-row">
<label class="sr-only" for="join-code">Lobby code</label>
<input type="text" id="join-code" placeholder="Enter Lobby ID"/> <input type="text" id="join-code" placeholder="Enter Lobby ID"/>
<button id="submit-join">Join</button> <button id="submit-join">Join</button>
</div> </div>
@@ -52,7 +54,7 @@
<p>No public lobbies found</p> <p>No public lobbies found</p>
</div> </div>
</div> </div>
<div id="join-error" class="error-message hidden"></div> <div id="join-error" role="alert" aria-live="polite" class="error-message hidden"></div>
<div id="lobby-room" class="lobby-room hidden"> <div id="lobby-room" class="lobby-room hidden">
<div class="lobby-header"> <div class="lobby-header">
<button id="leave-button" class="leave-button">Leave Lobby</button> <button id="leave-button" class="leave-button">Leave Lobby</button>
@@ -64,6 +66,8 @@
<button id="start-game" class="start-game hidden" disabled>Start Game</button> <button id="start-game" class="start-game hidden" disabled>Start Game</button>
</div> </div>
</body> </body>
<script src="protocol.js"></script>
<script src="client.js"></script>
<script src="frontend_home.js" defer></script> <script src="frontend_home.js" defer></script>
<script src="matrix.js"></script> <script src="matrix.js"></script>
</html> </html>
+7 -9
View File
@@ -4,26 +4,24 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title> <title>Login</title>
<link rel="stylesheet" href="shared.css">
<link rel="stylesheet" href="styles_login.css"> <link rel="stylesheet" href="styles_login.css">
<link rel="icon" href="gambling.png" type="image/x-icon"> <link rel="icon" href="gambling.png" type="image/x-icon">
</head> </head>
<body> <body>
<h1>Login / Register</h1> <h1>Login / Register</h1>
<form method="post" action="/login"> <form method="post" action="/login">
<input type="text" name="username" placeholder="Username"> <label for="username">Username</label>
<input type="password" name="password" placeholder="Password"> <input id="username" type="text" name="username" placeholder="Username" autocomplete="username" maxlength="40" required>
<label for="password">Password</label>
<input id="password" type="password" name="password" placeholder="Password" autocomplete="current-password" required aria-describedby="password-hint">
<small id="password-hint">Passwords support up to 72 UTF-8 bytes.</small>
<div class="auth-actions"> <div class="auth-actions">
<button type="submit" id="login-button">Login</button> <button type="submit" id="login-button">Login</button>
<button type="button" id="register-button">Register</button> <button type="button" id="register-button">Register</button>
</div> </div>
<p id="auth-error" class="auth-error hidden"></p> <p id="auth-error" role="alert" aria-live="polite" class="auth-error hidden"></p>
</form> </form>
<div id="loading-screen" class="hidden">
<div class="loading-text" id="loading-text">Loading...</div>
<div class="progress-bar">
<div class="progress" id="progress"></div>
</div>
</div>
</body> </body>
<script src="frontend_login.js"></script> <script src="frontend_login.js"></script>
</html> </html>
+12 -2
View File
@@ -5,6 +5,8 @@ const fontSize = 16;
const frameInterval = 65; const frameInterval = 65;
let drops = []; let drops = [];
let lastFrameTime = 0; let lastFrameTime = 0;
let animationFrame = null;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
function resizeCanvas() { function resizeCanvas() {
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
@@ -41,9 +43,17 @@ function animate(timestamp) {
drawMatrix(); drawMatrix();
lastFrameTime = timestamp; lastFrameTime = timestamp;
} }
requestAnimationFrame(animate); animationFrame = requestAnimationFrame(animate);
} }
function updateAnimation() {
cancelAnimationFrame(animationFrame);
animationFrame = null;
if (!document.hidden && !reducedMotion.matches) animationFrame = requestAnimationFrame(animate);
else ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
resizeCanvas(); resizeCanvas();
requestAnimationFrame(animate); updateAnimation();
document.addEventListener("visibilitychange", updateAnimation);
reducedMotion.addEventListener("change", updateAnimation);
window.addEventListener("resize", resizeCanvas); window.addEventListener("resize", resizeCanvas);
+7 -3
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Poker Table</title> <title>Poker Table</title>
<link rel="stylesheet" href="shared.css">
<link rel="stylesheet" href="styles_poker.css"> <link rel="stylesheet" href="styles_poker.css">
<link rel="icon" href="gambling.png" type="image/x-icon"> <link rel="icon" href="gambling.png" type="image/x-icon">
</head> </head>
@@ -57,17 +58,18 @@
</section> </section>
<section id="showdown-panel" class="showdown-panel hidden" aria-label="Showdown"></section> <section id="showdown-panel" class="showdown-panel hidden" aria-label="Showdown"></section>
<div id="error-message" class="error-message hidden"></div> <div id="error-message" role="alert" aria-live="polite" class="error-message hidden"></div>
</main> </main>
<div id="bet-modal" class="bet-modal hidden" role="dialog" aria-modal="true" aria-labelledby="bet-title"> <div id="bet-modal" class="bet-modal hidden" role="dialog" aria-modal="true" aria-labelledby="bet-title">
<div class="bet-panel"> <div class="bet-panel">
<button id="close-bet" class="close-bet" aria-label="Close bet dialog">x</button> <button id="close-bet" class="close-bet" aria-label="Close bet dialog">x</button>
<h2 id="bet-title">Choose amount to bet</h2> <h2 id="bet-title">Choose amount to bet</h2>
<label class="sr-only" for="bet-range">Chips to add to the pot</label>
<input id="bet-range" type="range" min="0" max="400" step="10" value="50"> <input id="bet-range" type="range" min="0" max="400" step="10" value="50">
<div class="range-labels"> <div class="range-labels">
<span>$0</span> <span id="range-min">$0</span>
<span>$400</span> <span id="range-max">$0</span>
</div> </div>
<div class="bet-amount"> <div class="bet-amount">
<button id="decrease-bet">-</button> <button id="decrease-bet">-</button>
@@ -78,6 +80,8 @@
</div> </div>
</div> </div>
<script src="protocol.js"></script>
<script src="client.js"></script>
<script src="frontend_poker.js" defer></script> <script src="frontend_poker.js" defer></script>
<script src="matrix.js"></script> <script src="matrix.js"></script>
</body> </body>
+13 -20
View File
@@ -3,7 +3,7 @@
color: #0f0; color: #0f0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0; margin: 0;
height: 100vh; min-height: 100vh;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -89,17 +89,6 @@ button.logout {
vertical-align: middle; vertical-align: middle;
} }
.lobby-room {
background: rgba(0, 0, 0, 0.7);
border: 2px solid #0f0;
border-radius: 12px;
padding: 30px;
margin-top: 40px;
width: 400px;
text-align: center;
box-shadow: 0 0 10px #0f0;
}
.lobby-room h2 { .lobby-room h2 {
color: #0f0; color: #0f0;
font-size: 20px; font-size: 20px;
@@ -111,10 +100,6 @@ button.logout {
color: #fff; color: #fff;
} }
.hidden {
display: none;
}
.join-browser { .join-browser {
margin-top: 20px; margin-top: 20px;
width: min(520px, calc(100vw - 32px)); width: min(520px, calc(100vw - 32px));
@@ -231,10 +216,6 @@ button.logout {
text-shadow: 0 0 3px red; text-shadow: 0 0 3px red;
} }
.hidden {
display: none;
}
#matrix-canvas { #matrix-canvas {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -341,3 +322,15 @@ button.logout {
box-shadow: 0 0 5px #0f0 inset; box-shadow: 0 0 5px #0f0 inset;
transform: none; transform: none;
} }
.lobby-room { width: min(460px, calc(100vw - 32px)); overflow-wrap: anywhere; }
body { padding: 72px 16px 24px; gap: 8px; }
@media (max-width: 1000px) {
body { justify-content: flex-start; }
.leaderboard { position: static; width: min(460px, 100%); order: 10; margin-top: 24px; }
body::before { text-align: center; font-size: 20px; margin-bottom: 12px; }
.join-code-row { flex-wrap: wrap; }
#join-form input { width: 100%; min-width: 0; }
.lobby-visibility { flex-wrap: wrap; justify-content: center; }
.lobby-header { position: static; text-align: right; }
}
+3 -40
View File
@@ -6,7 +6,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; min-height: 100vh;
margin: 0; margin: 0;
} }
@@ -82,43 +82,6 @@ button:disabled {
text-align: center; text-align: center;
} }
.hidden {
display: none !important;
}
#loading-screen { form { width: min(364px, calc(100vw - 32px)); }
position: fixed; body { padding: 24px 0; }
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #000;
color: #0f0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-text {
font-size: 24px;
margin-bottom: 20px;
text-shadow: 0 0 10px #0f0;
}
.progress-bar {
width: 80%;
height: 20px;
border: 2px solid #0f0;
border-radius: 10px;
background: #111;
}
.progress {
height: 100%;
width: 0;
background-color: #0f0;
border-radius: 10px;
transition: width 0.3s ease;
}
+16 -5
View File
@@ -4,7 +4,7 @@ body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow-x: clip;
} }
button { button {
@@ -348,10 +348,6 @@ button:disabled:hover {
padding: 8px 22px; padding: 8px 22px;
} }
.hidden {
display: none;
}
@media (max-width: 700px) { @media (max-width: 700px) {
.poker-table { .poker-table {
padding: 14px; padding: 14px;
@@ -386,3 +382,18 @@ button:disabled:hover {
justify-content: center; justify-content: center;
} }
} }
.poker-table { gap: 24px; }
.turn-panel { min-width: 0; width: min(460px, 100%); }
.community-area, .player-area { max-width: 100%; min-width: 0; }
.card-row { flex-wrap: wrap; justify-content: center; }
.bet-modal { z-index: 10; padding: 24px; }
.bet-panel { max-width: 100%; }
.player-status, .round-panel { overflow-wrap: anywhere; }
@media (max-width: 700px) {
.card-row { gap: 6px; }
.card-row .card { width: 48px; height: 74px; }
.showdown-panel { position: static; width: 100%; max-height: none; }
.round-panel { padding: 12px 8px; font-size: 14px; }
.divider { margin: 0 4px; }
}
+80 -101
View File
@@ -1,110 +1,89 @@
const {test, before, after} = require("node:test"); const {test} = require('node:test');
const assert = require("node:assert/strict"); const assert = require('node:assert/strict');
const {once} = require("node:events"); const {once} = require('node:events');
const {WebSocket} = require("ws"); const crypto = require('node:crypto');
process.env.DATABASE_PATH = ":memory:"; const {WebSocket} = require('ws');
process.env.TOKEN_SECRET = "isolated-test-secret"; const {createApplication} = require('../gambling');
const api = require("../gambling"); const {parseCookies} = require('../lib/transport');
let base; const secret = 'test-secret-with-at-least-thirty-two-characters';
const sessions = Object.create(null);
before(async () => {
await api.initializeDatabase({migrate: false});
api.server.listen(0, "127.0.0.1");
await once(api.server, "listening");
base = `http://127.0.0.1:${api.server.address().port}`;
});
after(() => api.close());
async function request(route, {user, method = "GET", body} = {}) {
return fetch(base + route, {method, headers: {
...(body !== undefined ? {"Content-Type": "application/json"} : {}),
...(user ? {Authorization: `Bearer ${sessions[user]}`} : {})
}, body: body === undefined ? undefined : JSON.stringify(body)});
}
function nextMessage(socket, type) { function nextMessage(socket, type) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timer = setTimeout(() => {socket.off("message", listener); reject(new Error(`Timeout: ${type}`));}, 3000); const timeout = setTimeout(() => {socket.off('message', listener); reject(new Error(`Timeout waiting for ${type}`));}, 3000);
function listener(raw) { const listener = raw => {const message = JSON.parse(raw); if (message.type !== type) return; clearTimeout(timeout); socket.off('message', listener); resolve(message);};
const value = JSON.parse(raw); socket.on('message', listener);
if (value.type !== type) return;
clearTimeout(timer); socket.off("message", listener); resolve(value);
}
socket.on("message", listener);
}); });
} }
test("HTTP authentication and WebSocket lobby lifecycle", async () => { async function fixture(t, options = {}) {
assert.equal((await request("/balance")).status, 401); const api = await createApplication({secret, databasePath: ':memory:', bcryptRounds: 4, intermissionMs: 60000, ...options});
assert.equal((await request("/register", {method: "POST"})).status, 400); await api.listen(0); t.after(() => api.close());
for (const user of ["alice", "bob", "__proto__"]) { const base = `http://127.0.0.1:${api.server.address().port}`;
const response = await request("/register", {method: "POST", body: {username: user, password: "test-password"}}); const request = async (route, {token, method = 'GET', body, headers = {}} = {}) => fetch(base + route, {method, redirect: 'manual',
assert.equal(response.status, 200); headers: {...(token ? {Authorization: `Bearer ${token}`} : {}), ...(body !== undefined ? {'Content-Type': 'application/json'} : {}), ...headers},
sessions[user] = (await response.json()).token; body: body === undefined ? undefined : JSON.stringify(body)});
} return {api, base, request};
assert.equal((await request("/balance", {user: "alice"})).status, 200); }
assert.equal((await request("/register", {method: "POST", body: {username: "long", password: "x".repeat(73)}})).status, 400); test('HTTP contract, sessions, origin checks, and socket lifecycle', async t => {
const oldToken = sessions.alice; const {api, base, request} = await fixture(t);
const login = await request("/login", {method: "POST", body: {username: " ALICE ", password: "test-password"}}); assert.equal((await request('/balance')).status, 401);
sessions.alice = (await login.json()).token; assert.equal((await request('/poker.html')).status, 302);
assert.notEqual(sessions.alice, oldToken); assert.equal((await request('/register', {method: 'POST'})).status, 400);
assert.equal((await request("/lobby/join?lobbyId=__proto__", {user: "alice"})).status, 404); assert.equal((await request('/lobby/create')).status, 405);
const socket = new WebSocket(base.replace("http", "ws"), {headers: {Authorization: `Bearer ${sessions.alice}`, Cookie: "bad=%ZZ"}}); const registered = await request('/register', {method: 'POST', body: {username: '__proto__', password: 'password'}});
await nextMessage(socket, "auth:success"); const {token} = await registered.json();
let pending = nextMessage(socket, "lobby:state"); assert.equal(registered.status, 200);
socket.send(JSON.stringify({type: "lobby:create"})); assert.match(registered.headers.get('set-cookie'), /HttpOnly/);
const lobby = await pending; assert.notEqual((await api.repository.user('__proto__')).sessionHash, token);
assert.deepEqual(lobby.players, ["alice"]); assert.equal((await request('/lobby/create', {method: 'POST', token, headers: {Origin: 'https://evil.invalid'}, body: {requestId: crypto.randomUUID()}})).status, 403);
assert.equal((await request("/lobby/create", {user: "alice"})).status, 409); const bob = await api.sessions.register({username: 'bob', password: 'password'});
assert.equal((await request(`/lobby/join?lobbyId=${lobby.lobbyId}`, {user: "bob"})).status, 200); const socket = new WebSocket(base.replace('http', 'ws'), {headers: {Authorization: `Bearer ${token}`, Cookie: 'bad=%ZZ'}});
pending = nextMessage(socket, "poker:started"); await nextMessage(socket, 'auth:success');
socket.send(JSON.stringify({type: "poker:start"})); let pending = nextMessage(socket, 'command:ack');
await pending; socket.send(JSON.stringify({type: 'lobby:create', requestId: crypto.randomUUID()}));
pending = nextMessage(socket, "poker:error"); const created = await pending;
socket.send(JSON.stringify({type: "poker:start"})); assert.equal(created.commandType, 'lobby:create');
assert.match((await pending).message, /already started/); assert.equal((await request('/lobby/join', {method: 'POST', token: bob.token, body: {lobbyId: created.lobbyId, requestId: crypto.randomUUID()}})).status, 200);
pending = nextMessage(socket, "error"); const start = {type: 'poker:start', lobbyId: created.lobbyId, requestId: crypto.randomUUID()};
socket.send("null"); pending = nextMessage(socket, 'command:ack'); socket.send(JSON.stringify(start)); await pending;
assert.match((await pending).message, /object/); pending = nextMessage(socket, 'error'); socket.send(JSON.stringify({...start, requestId: crypto.randomUUID()}));
const closed = once(socket, "close"); assert.equal((await pending).code, 'GAME_STARTED');
await request("/logout", {user: "alice", method: "DELETE"}); pending = nextMessage(socket, 'error'); socket.send('null'); assert.equal((await pending).code, 'INVALID_MESSAGE');
socket.send(JSON.stringify({type: "lobby:create"})); const closed = once(socket, 'close');
await closed; await request('/logout', {method: 'DELETE', token});
assert.equal((await request("/balance", {user: "alice"})).status, 403); assert.equal((await closed)[0], 1008);
assert.equal((await request('/balance', {token})).status, 401);
}); });
test("leaving before the current seat preserves turn and independent lobby roster", async () => { test('login rotation immediately closes old sockets; proxy headers are untrusted by default', async t => {
const id = "test-table"; const {api, base, request} = await fixture(t);
api.playerMap[id] = ["alice", "bob", "__proto__"]; const user = await api.sessions.register({username: 'alice', password: 'password'});
const game = await api.createPokerGame(id); const socket = new WebSocket(base.replace('http', 'ws'), {headers: {Authorization: `Bearer ${user.token}`}});
assert.equal(game.players[game.currentPlayerIndex], "bob"); await nextMessage(socket, 'auth:success');
assert.notEqual(game.players, api.playerMap[id]); const closed = once(socket, 'close');
await api.removePlayerFromPokerGame(id, "alice"); const response = await request('/login', {method: 'POST', headers: {'X-Forwarded-Proto': 'https'}, body: {username: 'ALICE', password: 'password'}});
assert.equal(game.players[game.currentPlayerIndex], "bob"); assert.equal(response.status, 200); assert.doesNotMatch(response.headers.get('set-cookie'), /; Secure/);
assert.equal(game.contributions.alice, 10); assert.equal((await closed)[0], 1008);
api.clearTurnTimer(game); assert.equal(await api.sessions.verify(user.token), null);
}); });
test("fractional and nonnumeric bets do not mutate the pot", () => { test('authentication is rate limited and cookies tolerate invalid escapes', async t => {
const game = api.pokerGames["test-table"]; const {request} = await fixture(t, {authLimit: 2});
const pot = game.pot; for (let index = 0; index < 2; index++) assert.equal((await request('/login', {method: 'POST', body: {username: 'missing', password: 'password'}})).status, 401);
for (const amount of [0.5, "10", null, Infinity, -1]) { const limited = await request('/login', {method: 'POST', body: {username: 'missing', password: 'password'}});
assert.match(api.applyPokerAction(game, "bob", "bet", {amount}).error, /whole number/); assert.equal(limited.status, 429); assert.ok(limited.headers.has('retry-after'));
assert.equal(game.pot, pot); assert.equal(parseCookies('bad=%ZZ; authorization=ok').authorization, 'ok');
}
}); });
test("malformed cookie encoding is ignored", () => { test('socket expires without waiting for a client message', async t => {
assert.equal(api.parseCookieHeader("bad=%ZZ; authorization=valid").authorization, "valid"); const {api, base} = await fixture(t, {sessionSeconds: 1});
const {token} = await api.sessions.register({username: 'alice', password: 'password'});
const socket = new WebSocket(base.replace('http', 'ws'), {headers: {Authorization: `Bearer ${token}`}});
const closed = once(socket, 'close');
await nextMessage(socket, 'auth:success');
assert.equal((await closed)[0], 1008);
}); });
test('WebSocket rejects cross-origin upgrades', async t => {
test("ante-only all-ins run out the board without waiting for a broke player", async () => { const {api, base} = await fixture(t);
const id = "all-in-table"; const {token} = await api.sessions.register({username: 'alice', password: 'password'});
api.playerMap[id] = ["alice", "bob"]; const socket = new WebSocket(base.replace('http', 'ws'), {headers: {Authorization: `Bearer ${token}`, Origin: 'https://evil.invalid'}});
api.pokerGames[id] = {round: 1, phase: "intermission", dealerIndex: 0, stacks: {alice: 10, bob: 10}}; socket.on('error', () => {});
const game = await api.createPokerGame(id); const [, response] = await once(socket, 'unexpected-response');
assert.equal(game.round, 1); assert.equal(response.statusCode, 403); response.resume(); socket.terminate();
assert.equal(game.communityCards.length, 5);
assert.equal(game.potAwarded, true);
assert.equal(game.stacks.alice + game.stacks.bob, 20);
// Allow the asynchronous persistence and intermission transition to complete.
for (let i = 0; i < 100 && api.pokerGames[id] === game; i++) {
await new Promise(resolve => setTimeout(resolve, 5));
}
assert.equal(api.pokerGames[id].phase, "intermission");
assert.equal(api.pokerGames[id].round, 2);
}); });
module.exports = {fixture};