Sigma Ligma on the wall

This commit is contained in:
2026-05-05 11:24:34 +02:00
parent 4a977ce9b6
commit f57d326dd2
13 changed files with 2926 additions and 221 deletions
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>
+1 -19
View File
@@ -1,19 +1 @@
[
{
"username": "jerome",
"password": "Sigma",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb3dlcl91c2VybmFtZSI6Implcm9tZSIsImlhdCI6MTc0NDg4OTY3MSwiZXhwIjoxNzQ0ODkxNDcxfQ.zYg3uxh_iNZ2xdEma8p3p-URT3Xa7wnYiQyOx0-QTac",
"money": 400
},
{
"username": "Gianimon",
"password": "gianimon",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkdpYW5pbW9uIiwiaWF0IjoxNzQ0ODc0NTczLCJleHAiOjE3NDQ4NzYzNzN9.dGp5WTKIDVhomKZ8J7nevz8O7PqwRhR9wEgeyDtL_XE",
"money": 300
},
{
"username": "any",
"password": "any",
"money": 300
}
]
[]
+1200 -87
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -12,11 +12,15 @@
},
"private": true,
"dependencies": {
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"swagger-ui-express": "^5.0.1"
"sqlite": "^5.1.1",
"sqlite3": "^6.0.1",
"swagger-ui-express": "^5.0.1",
"ws": "^8.20.0"
},
"devDependencies": {
"swagger-autogen": "^2.23.7"
+265 -94
View File
@@ -3,50 +3,204 @@ document.addEventListener("DOMContentLoaded", async () => {
const balance = document.getElementById("balance");
const hostButton = document.getElementById("host");
const joinButton = document.getElementById("join");
const hostForm = document.getElementById("host-form");
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");
let playerUpdateInterval = null;
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 startPlayerUpdates = (lobbyId) => {
if (playerUpdateInterval) {
clearInterval(playerUpdateInterval);
}
playerUpdateInterval = setInterval(async () => {
const res = await fetch(`/lobby/players?lobbyId=${lobbyId}`);
const data = await res.json();
const playerList = document.getElementById("player-list");
playerList.innerHTML = data.players.map(p => `<p>${p}</p>`).join("") || "<p>No players yet</p>";
}, 3000);
const showJoinError = (message) => {
joinError.textContent = message;
joinError.classList.remove("hidden");
};
leaveButton.addEventListener("click", async () => {
const username = await getUsernameFromToken();
const lobbyId = document.getElementById("lobby-id").textContent;
try {
const res = await fetch(`/lobby/leave?lobbyId=${lobbyId}&username=${username}`, {
method: "DELETE",
});
if (res.ok) {
// Stoppe das Interval beim Verlassen
if (playerUpdateInterval) {
clearInterval(playerUpdateInterval);
playerUpdateInterval = null;
}
document.getElementById("lobby-room").classList.add("hidden");
document.getElementById("player-list").innerHTML = "<p>Waiting for players...</p>";
document.getElementById("lobby-id").textContent = "N/A";
} else {
const err = await res.json();
alert("Failed to leave lobby: " + (err.error || "Unknown error"));
}
} catch (err) {
console.error("Error leaving lobby:", err);
alert("Could not leave lobby.");
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");
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 = () => {
currentLobbyId = null;
lobbyRoom.classList.add("hidden");
lobbyIdDisplay.textContent = "N/A";
playerList.innerHTML = "<p>Waiting for players...</p>";
startGameButton.classList.add("hidden");
startGameButton.disabled = true;
};
const sendSocketMessage = async (message) => {
const ws = await connectSocket();
ws.send(JSON.stringify(message));
};
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 === "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 {
@@ -54,24 +208,35 @@ document.addEventListener("DOMContentLoaded", async () => {
const data = await res.json();
return data.user?.username;
} catch (err) {
console.error("Failed to get username from token");
console.error("Failed to get username from token", err);
return null;
}
};
try {
const balanceRes = await fetch('/balance', {method: "GET"});
await connectSocket();
currentUsername = await getUsernameFromToken();
} catch (error) {
showJoinError("Real-time poker connection failed.");
}
try {
const balanceRes = await fetch("/balance", {method: "GET"});
if (balanceRes.ok) {
const balanceData = await balanceRes.json();
balance.innerHTML = balanceData.balance;
balance.textContent = balanceData.balance;
}
} catch (error) {
console.log("Balance error:", error);
}
logoutButton.addEventListener("click", async (e) => {
e.preventDefault();
try {
const logoutRes = await fetch('/logout', {method: "DELETE"});
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
}
const logoutRes = await fetch("/logout", {method: "DELETE"});
if (logoutRes.ok) {
document.cookie = "authorization=; Max-Age=0; path=/;";
window.location.href = "/login.html";
@@ -80,74 +245,80 @@ document.addEventListener("DOMContentLoaded", async () => {
console.log("Logout error:", err);
}
});
// Im hostButton Event Listener
hostButton.addEventListener("click", async (e) => {
e.preventDefault();
const username = await getUsernameFromToken();
if (!username) return alert("User not authenticated");
try {
const res = await fetch(`/lobby/create?username=${username}`);
if (res.redirected) {
const joinURL = new URL(res.url);
const lobbyId = joinURL.searchParams.get("lobbyId");
const joinRes = await fetch(joinURL.href);
const {lobbyId: finalLobbyId} = await joinRes.json();
document.getElementById("lobby-id").textContent = finalLobbyId;
document.getElementById("lobby-room").classList.remove("hidden");
// ... bisheriger Code bis zum Interval ...
startPlayerUpdates(finalLobbyId);
}
} catch (err) {
console.error("Lobby create/join error:", err);
}
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();
if (joinForm.classList.contains("hidden")) {
joinForm.classList.remove("hidden");
joinForm.classList.toggle("hidden");
hostForm.classList.add("hidden");
if (!joinForm.classList.contains("hidden")) {
joinCodeInput.focus();
} else {
joinForm.classList.add("hidden");
loadPublicLobbies();
}
});
// Im joinSubmit Event Listener
refreshLobbies.addEventListener("click", (e) => {
e.preventDefault();
loadPublicLobbies();
});
joinSubmit.addEventListener("click", async (e) => {
e.preventDefault();
clearJoinError();
const lobbyId = joinCodeInput.value.trim();
const joinError = document.getElementById("join-error");
joinError.classList.add("hidden"); // hide any previous error
joinError.textContent = "";
if (!lobbyId) {
joinError.textContent = "Please enter a lobby ID.";
joinError.classList.remove("hidden");
showJoinError("Please enter a lobby ID.");
return;
}
const username = await getUsernameFromToken();
if (!username) {
joinError.textContent = "User not authenticated.";
joinError.classList.remove("hidden");
if (!currentUsername) {
showJoinError("User not authenticated.");
return;
}
try {
const joinRes = await fetch(`/lobby/join?lobbyId=${lobbyId}&username=${username}`);
if (joinRes.ok) {
const {lobbyId: finalLobbyId} = await joinRes.json();
document.getElementById("lobby-id").textContent = finalLobbyId;
document.getElementById("lobby-room").classList.remove("hidden");
joinForm.classList.add("hidden");
joinCodeInput.value = "";
// ... bisheriger Code bis zum Interval ...
startPlayerUpdates(finalLobbyId);
} else {
const error = await joinRes.json();
joinError.textContent = "Join failed: " + (error.error || "Unknown error");
joinError.classList.remove("hidden");
}
} catch (err) {
console.error("Join lobby error:", err);
joinError.textContent = "Could not join lobby. Please try again.";
joinError.classList.remove("hidden");
}
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() {
@@ -164,7 +335,7 @@ document.addEventListener("DOMContentLoaded", async () => {
item.textContent = `${i + 1}. ${player.username} - $${player.money}`;
if (player.username === data.self.username) {
item.classList.add("you");
item.textContent += " 👑";
item.textContent += " (you)";
}
list.appendChild(item);
});
@@ -179,4 +350,4 @@ document.addEventListener("DOMContentLoaded", async () => {
}
await loadLeaderboard();
});
});
+342
View File
@@ -0,0 +1,342 @@
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 = `<span>${card.rank}</span><span>${card.suit}</span>`;
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");
});
});
+27 -4
View File
@@ -25,9 +25,31 @@
<button id="host">Host Lobby</button>
<button id="join">Join Lobby</button>
</div>
<div id="join-form" class="hidden">
<input type="text" id="join-code" placeholder="Enter Lobby ID"/>
<button id="submit-join">Join</button>
<div id="host-form" class="host-form hidden">
<div class="lobby-visibility" aria-label="Lobby visibility">
<label>
<input type="radio" name="lobby-visibility" value="public" checked/>
Public Lobby
</label>
<label>
<input type="radio" name="lobby-visibility" value="private"/>
Private Lobby
</label>
</div>
<button id="create-lobby" type="button">Create Lobby</button>
</div>
<div id="join-form" class="join-browser hidden">
<div class="join-code-row">
<input type="text" id="join-code" placeholder="Enter Lobby ID"/>
<button id="submit-join">Join</button>
</div>
<div class="browser-header">
<h2>Public Lobbies</h2>
<button id="refresh-lobbies" type="button">Refresh</button>
</div>
<div id="public-lobbies" class="public-lobbies">
<p>No public lobbies found</p>
</div>
</div>
<div id="join-error" class="error-message hidden"></div>
<div id="lobby-room" class="lobby-room hidden">
@@ -38,8 +60,9 @@
<div id="player-list">
<p>Waiting for players...</p>
</div>
<button id="start-game" class="start-game hidden" disabled>Start Game</button>
</div>
</body>
<script src="frontend_home.js" defer></script>
<script src="matrix.js"></script>
</html>
</html>
+34 -12
View File
@@ -1,27 +1,49 @@
const canvas = document.getElementById("matrix-canvas");
const canvas = document.getElementById("matrix-canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const letters = "アァイィウヴエェオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
const fontSize = 16;
const columns = Math.floor(canvas.width / fontSize);
const drops = new Array(columns).fill(1);
const frameInterval = 65;
let drops = [];
let lastFrameTime = 0;
function resizeCanvas() {
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = Math.floor(width * pixelRatio);
canvas.height = Math.floor(height * pixelRatio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
const columns = Math.ceil(width / fontSize);
drops = new Array(columns).fill(1);
}
function drawMatrix() {
ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
ctx.fillStyle = "#39FF14";
ctx.font = fontSize + "px monospace";
for (let i = 0; i < drops.length; i++) {
const text = letters[Math.floor(Math.random() * letters.length)];
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height || Math.random() > 0.975) {
if (drops[i] * fontSize > window.innerHeight || Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
}
setInterval(drawMatrix, 50);
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
function animate(timestamp) {
if (timestamp - lastFrameTime >= frameInterval) {
drawMatrix();
lastFrameTime = timestamp;
}
requestAnimationFrame(animate);
}
resizeCanvas();
requestAnimationFrame(animate);
window.addEventListener("resize", resizeCanvas);
+84
View File
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Poker Table</title>
<link rel="stylesheet" href="styles_poker.css">
<link rel="icon" href="gambling.png" type="image/x-icon">
</head>
<body>
<canvas id="matrix-canvas"></canvas>
<main class="poker-table">
<header class="table-header">
<div class="round-panel">
<span>Round <strong id="round">1</strong></span>
<span class="divider">|</span>
<span id="street">Preflop</span>
<span class="divider">|</span>
<span>Pot: $<strong id="pot">0</strong></span>
</div>
<button id="leave-table" class="leave-table">Leave Table</button>
</header>
<section class="community-area" aria-label="Community cards">
<div class="turn-panel">
<span>Turn: <strong id="current-turn">Waiting...</strong></span>
<span id="turn-state">Waiting for table state</span>
</div>
<ol id="turn-queue" class="turn-queue"></ol>
<div id="community-cards" class="card-row">
<div class="card placeholder">?</div>
<div class="card placeholder">?</div>
<div class="card placeholder">?</div>
</div>
</section>
<section class="actions" aria-label="Poker actions">
<button data-action="hit">Hit</button>
<button data-action="fold">Fold</button>
<button data-action="bet">Bet</button>
</section>
<section class="player-area" aria-label="Your cards">
<div id="player-cards" class="hand">
<div class="card player-card placeholder">Card 1</div>
<div class="card player-card placeholder">Card 2</div>
</div>
<div id="spectator-note" class="spectator-note hidden">Spectating</div>
<div class="player-status">
<span id="player-name">Player</span>
<span>Your Bet: $<strong id="current-bet">0</strong></span>
<span>Your Total: $<strong id="your-contribution">0</strong></span>
<span>Your Stack: $<strong id="your-stack">400</strong></span>
<span id="last-action">Game starting</span>
</div>
</section>
<section id="showdown-panel" class="showdown-panel hidden" aria-label="Showdown"></section>
<div id="error-message" class="error-message hidden"></div>
</main>
<div id="bet-modal" class="bet-modal hidden" role="dialog" aria-modal="true" aria-labelledby="bet-title">
<div class="bet-panel">
<button id="close-bet" class="close-bet" aria-label="Close bet dialog">x</button>
<h2 id="bet-title">Choose amount to bet</h2>
<input id="bet-range" type="range" min="0" max="400" step="10" value="50">
<div class="range-labels">
<span>$0</span>
<span>$400</span>
</div>
<div class="bet-amount">
<button id="decrease-bet">-</button>
<span>$<strong id="bet-value">50</strong></span>
<button id="increase-bet">+</button>
</div>
<button id="confirm-bet" class="confirm-bet">Confirm</button>
</div>
</div>
<script src="frontend_poker.js" defer></script>
<script src="matrix.js"></script>
</body>
</html>
+125 -4
View File
@@ -58,6 +58,37 @@ button.logout {
margin-top: 30px;
}
.host-form {
display: grid;
gap: 12px;
justify-items: center;
margin-top: 18px;
}
.lobby-visibility {
color: #fff;
font-family: monospace;
display: flex;
align-items: center;
gap: 8px;
text-shadow: 0 0 3px #0f0;
}
.lobby-visibility label {
border: 1px solid rgba(0, 255, 0, 0.5);
border-radius: 8px;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.65);
cursor: pointer;
}
.lobby-visibility input {
width: 16px;
height: 16px;
accent-color: #0f0;
vertical-align: middle;
}
.lobby-room {
background: rgba(0, 0, 0, 0.7);
border: 2px solid #0f0;
@@ -84,14 +115,24 @@ button.logout {
display: none;
}
#join-form {
.join-browser {
margin-top: 20px;
width: min(520px, calc(100vw - 32px));
background: rgba(0, 0, 0, 0.78);
border: 2px solid #0f0;
border-radius: 10px;
box-shadow: 0 0 10px #0f0;
padding: 18px;
box-sizing: border-box;
}
.join-code-row {
display: flex;
gap: 10px;
display: none; /* default hidden */
}
#join-form:not(.hidden) {
display: flex;
display: block;
}
#join-form input {
@@ -105,6 +146,73 @@ button.logout {
width: 200px;
}
.browser-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
border-top: 1px solid rgba(0, 255, 0, 0.35);
padding-top: 14px;
}
.browser-header h2 {
margin: 0;
font-size: 18px;
color: #0f0;
font-family: monospace;
}
.browser-header button,
.public-lobby button {
padding: 8px 14px;
font-size: 13px;
border-radius: 8px;
}
.public-lobbies {
display: grid;
gap: 10px;
margin-top: 12px;
max-height: 220px;
overflow: auto;
}
.public-lobbies p {
margin: 0;
color: #fff;
font-family: monospace;
}
.public-lobby {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 12px;
border: 1px solid rgba(0, 255, 0, 0.45);
border-radius: 8px;
padding: 10px;
background: rgba(0, 255, 0, 0.06);
}
.public-lobby-info {
display: grid;
gap: 4px;
color: #fff;
font-family: monospace;
min-width: 0;
}
.public-lobby-info strong {
color: #0f0;
overflow-wrap: anywhere;
}
.public-lobby-info span {
font-size: 13px;
color: #d8ffd8;
}
#join-form button {
background-color: #0f0;
color: #000;
@@ -219,4 +327,17 @@ button.logout {
.leave-button:hover {
background-color: #c00;
transform: scale(1.05);
}
}
.start-game {
margin-top: 20px;
width: 100%;
}
.start-game:disabled {
background-color: #163016;
color: #67a667;
cursor: not-allowed;
box-shadow: 0 0 5px #0f0 inset;
transform: none;
}
+388
View File
@@ -0,0 +1,388 @@
body {
background: radial-gradient(circle at top, #111, #000);
color: #0f0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
min-height: 100vh;
overflow: hidden;
}
button {
background-color: #0f0;
color: #000;
border: none;
padding: 10px 18px;
font-size: 14px;
font-weight: bold;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 0 10px #0f0, 0 0 5px #0f0 inset;
transition: background 0.2s ease-in-out, transform 0.2s;
}
button:hover {
background-color: #1f1;
transform: scale(1.05);
}
button:disabled {
background-color: #163016;
color: #67a667;
cursor: not-allowed;
box-shadow: 0 0 5px #0f0 inset;
transform: none;
}
button:disabled:hover {
background-color: #163016;
transform: none;
}
#matrix-canvas {
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100vw;
height: 100vh;
background: transparent;
pointer-events: none;
}
.poker-table {
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto 1.5fr;
align-items: center;
justify-items: center;
padding: 24px;
box-sizing: border-box;
}
.table-header {
width: 100%;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: start;
}
.round-panel {
grid-column: 2;
border: 2px solid #0f0;
background: rgba(0, 0, 0, 0.78);
color: #0f0;
box-shadow: 0 0 12px #0f0;
padding: 16px 28px;
font-family: monospace;
font-size: 20px;
text-shadow: 0 0 5px #0f0;
}
.divider {
margin: 0 14px;
color: #fff;
}
.leave-table {
grid-column: 3;
justify-self: end;
background-color: #111;
color: #0f0;
border: 1px solid #0f0;
}
.card-row,
.hand,
.actions,
.player-status,
.bet-amount,
.range-labels {
display: flex;
align-items: center;
}
.card-row {
gap: 14px;
}
.community-area {
display: grid;
justify-items: center;
gap: 18px;
}
.turn-panel {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: center;
justify-content: center;
min-width: min(460px, calc(100vw - 32px));
border: 1px solid #0f0;
background: rgba(0, 0, 0, 0.78);
box-shadow: 0 0 8px #0f0;
color: #fff;
padding: 10px 16px;
font-family: monospace;
}
.turn-queue {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
padding: 0;
margin: 0;
list-style: none;
font-family: monospace;
}
.turn-queue li {
border: 1px solid #0f0;
background: rgba(0, 0, 0, 0.75);
color: #fff;
box-shadow: 0 0 6px #0f0;
padding: 6px 10px;
}
.turn-queue li.active {
background: #0f0;
color: #000;
}
.card {
width: 78px;
height: 116px;
background: #f4fff4;
color: #050505;
border: 2px solid #0f0;
border-radius: 6px;
box-shadow: 0 0 10px #0f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: monospace;
font-size: 22px;
font-weight: bold;
}
.card.red {
color: #b00020;
}
.card.placeholder {
background: #0b0b0b;
color: #0f0;
font-size: 14px;
text-align: center;
text-shadow: 0 0 5px #0f0;
}
.actions {
gap: 12px;
}
.player-area {
align-self: end;
display: grid;
justify-items: center;
gap: 18px;
padding-bottom: 24px;
}
.hand {
gap: 22px;
}
.player-card:first-child {
transform: rotate(-8deg);
}
.player-card:last-child {
transform: rotate(9deg);
}
.player-status {
gap: 20px;
flex-wrap: wrap;
justify-content: center;
background: rgba(0, 0, 0, 0.78);
border: 1px solid #0f0;
box-shadow: 0 0 8px #0f0;
padding: 10px 18px;
font-family: monospace;
color: #fff;
}
.spectator-note {
border: 1px solid #0f0;
background: rgba(0, 0, 0, 0.78);
box-shadow: 0 0 8px #0f0;
color: #fff;
font-family: monospace;
padding: 8px 14px;
}
.error-message {
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
color: red;
font-weight: bold;
text-shadow: 0 0 3px red;
}
.showdown-panel {
position: fixed;
right: 24px;
bottom: 24px;
width: min(340px, calc(100vw - 48px));
max-height: 42vh;
overflow: auto;
background: rgba(0, 0, 0, 0.88);
border: 2px solid #0f0;
box-shadow: 0 0 12px #0f0;
padding: 14px;
font-family: monospace;
}
.showdown-panel h2 {
margin: 0 0 10px;
font-size: 18px;
color: #0f0;
}
.showdown-row {
display: grid;
gap: 8px;
padding: 10px 0;
border-top: 1px solid rgba(0, 255, 0, 0.35);
color: #fff;
}
.mini-cards {
display: flex;
gap: 6px;
}
.mini-cards .card {
width: 38px;
height: 54px;
font-size: 13px;
box-shadow: 0 0 5px #0f0;
}
.bet-modal {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.28);
}
.bet-panel {
position: relative;
width: min(360px, calc(100vw - 32px));
background: rgba(0, 0, 0, 0.92);
border: 2px solid #0f0;
box-shadow: 0 0 16px #0f0;
color: #0f0;
padding: 28px;
text-align: center;
font-family: monospace;
}
.bet-panel h2 {
margin: 0 0 18px;
font-size: 16px;
text-shadow: 0 0 5px #0f0;
}
.close-bet {
position: absolute;
top: -14px;
right: -14px;
width: 34px;
height: 34px;
padding: 0;
background: #f00;
color: #fff;
border-radius: 50%;
box-shadow: 0 0 8px #f00;
}
#bet-range {
width: 100%;
accent-color: #0f0;
}
.range-labels {
justify-content: space-between;
color: #fff;
margin-bottom: 12px;
}
.bet-amount {
justify-content: center;
gap: 12px;
color: #fff;
margin-bottom: 12px;
}
.bet-amount span {
min-width: 90px;
border: 1px solid #0f0;
padding: 8px 12px;
background: #111;
}
.bet-amount button {
width: 34px;
height: 34px;
padding: 0;
}
.confirm-bet {
padding: 8px 22px;
}
.hidden {
display: none;
}
@media (max-width: 700px) {
.poker-table {
padding: 14px;
grid-template-rows: auto 1fr auto 1.2fr;
}
.table-header {
grid-template-columns: 1fr;
gap: 14px;
justify-items: center;
}
.round-panel,
.leave-table {
grid-column: 1;
justify-self: center;
}
.round-panel {
font-size: 16px;
padding: 12px 16px;
}
.card {
width: 58px;
height: 88px;
font-size: 18px;
}
.actions {
flex-wrap: wrap;
justify-content: center;
}
}
+443
View File
@@ -0,0 +1,443 @@
{
"openapi": "3.0.0",
"info": {
"title": "GAMBLE!",
"description": "Gambling API",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:25565/"
}
],
"paths": {
"/register": {
"post": {
"description": "",
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"example": "any"
},
"password": {
"example": "any"
}
}
}
}
}
}
}
},
"/leaderboard": {
"get": {
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/login": {
"get": {
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "",
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"example": "any"
},
"password": {
"example": "any"
}
}
}
}
}
}
}
},
"/": {
"get": {
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/balance": {
"get": {
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/verify": {
"post": {
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/logout": {
"delete": {
"description": "",
"parameters": [
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/leave-lobby": {
"post": {
"description": "",
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"lobbyId": {
"example": "any"
},
"username": {
"example": "any"
}
}
}
}
}
}
}
},
"/lobby/create": {
"get": {
"description": "",
"parameters": [
{
"name": "username",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/lobby/join": {
"get": {
"description": "",
"parameters": [
{
"name": "lobbyId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "username",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/lobby/leave": {
"delete": {
"description": "",
"parameters": [
{
"name": "lobbyId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "username",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/lobby/players": {
"get": {
"description": "",
"parameters": [
{
"name": "lobbyId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "authorization",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"bearerAuth": []
}
]
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
}
}