diff --git a/backend/package.json b/backend/package.json index b5d42ee..ed25459 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,8 @@ "dotenv": "^16.4.0", "express": "^4.19.0", "express-session": "^1.17.3", - "pg": "^8.12.0" + "pg": "^8.12.0", + "ws": "^8.18.0" }, "devDependencies": { "@types/connect-pg-simple": "^7.0.3", @@ -25,6 +26,7 @@ "@types/express-session": "^1.17.8", "@types/node": "^22.0.0", "@types/pg": "^8.11.6", + "@types/ws": "^8.5.11", "ts-node-dev": "^2.0.0", "typescript": "^5.6.0" } diff --git a/backend/src/index.ts b/backend/src/index.ts index 400a96b..9f70c35 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,7 @@ import express from "express"; +import http from "http"; +import { WebSocketServer } from "ws"; +import { handleConnection } from "./services/crashGame"; import session from "express-session"; import cors from "cors"; import cookieParser from "cookie-parser"; @@ -16,10 +19,10 @@ import { import { adminRouter } from "./routes/admin"; const app = express(); +const server = http.createServer(app); +const wss = new WebSocketServer({ server }); +wss.on('connection', handleConnection); -// --- ÄNDERUNG 1: Robustere Proxy-Erkennung --- -// Vertraue dem X-Forwarded-For Header, der von den Proxys gesetzt wird. -// Das ist zuverlässiger als die Anzahl der Proxys zu raten. app.set("trust proxy", true); diff --git a/backend/src/services/crashGame.ts b/backend/src/services/crashGame.ts new file mode 100644 index 0000000..2abf3a0 --- /dev/null +++ b/backend/src/services/crashGame.ts @@ -0,0 +1,105 @@ +import { WebSocket } from "ws"; +import { pool } from "../db"; + +// Typen für den Spielzustand +type GamePhase = "waiting" | "betting" | "running" | "crashed"; + +interface Player { + ws: WebSocket; + userId: number; + bet: number; + cashedOutAt?: number; +} + +// Spielzustand +let phase: GamePhase = "waiting"; +let players: Map = new Map(); +let multiplier = 1.0; +let crashPoint = 0; +let roundStartTime = 0; + +// --- WebSocket-Verwaltung --- +const clients = new Set(); + +export function handleConnection(ws: WebSocket) { + clients.add(ws); + console.log("[Crash] New client connected."); + + // Sende den aktuellen Zustand an den neuen Client + ws.send(JSON.stringify({ + type: "gameState", + phase, + multiplier, + players: Array.from(players.values()).map(p => ({ userId: p.userId, bet: p.bet, cashedOutAt: p.cashedOutAt })) + })); + + ws.on("message", (message) => { + // Hier werden wir später die "bet" und "cashout" Nachrichten verarbeiten + }); + + ws.on("close", () => { + clients.delete(ws); + players.delete(ws); // Spieler bei Disconnect entfernen + console.log("[Crash] Client disconnected."); + }); +} + +function broadcast(message: object) { + const data = JSON.stringify(message); + for (const client of clients) { + client.send(data); + } +} + +// --- Spiellogik --- + +function calculateCrashPoint(): number { + // Dies ist die "geheime Zutat". Ein guter Crash-Algorithmus ist entscheidend. + // Wir verwenden hier eine einfache Formel für den Anfang. + const r = Math.random(); + // Die Formel sorgt dafür, dass niedrige Multiplikatoren viel häufiger sind. + const crash = 1 / (1 - r); + return Math.max(1.01, parseFloat(crash.toFixed(2))); +} + +async function runGameLoop() { + while (true) { + // 1. Betting Phase (10 Sekunden) + phase = "betting"; + crashPoint = calculateCrashPoint(); + console.log(`[Crash] New round starting. Crash point will be: ${crashPoint}x`); + broadcast({ type: "newRound", phase: "betting", duration: 10000 }); + await new Promise(resolve => setTimeout(resolve, 10000)); + + // 2. Running Phase + phase = "running"; + roundStartTime = Date.now(); + broadcast({ type: "roundStart", phase: "running" }); + + while (multiplier < crashPoint) { + const elapsed = (Date.now() - roundStartTime) / 1000; + // Der Multiplikator steigt exponentiell an, um es spannender zu machen + multiplier = parseFloat(Math.pow(1.05, elapsed).toFixed(2)); + + broadcast({ type: "multiplierUpdate", multiplier }); + await new Promise(resolve => setTimeout(resolve, 100)); // Update alle 100ms + } + + // 3. Crashed Phase + phase = "crashed"; + multiplier = crashPoint; + broadcast({ type: "crash", multiplier: crashPoint }); + + // TODO: Gewinne an die Spieler auszahlen, die gecashed haben. + // TODO: Einsätze von Spielern abziehen, die nicht gecashed haben. + + await new Promise(resolve => setTimeout(resolve, 5000)); // 5s Pause + + // Reset für die nächste Runde + players.clear(); + multiplier = 1.0; + } +} + +// Starte den Spiel-Loop +runGameLoop(); \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1e37262..bfa6a13 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { Routes, Route, Link, Navigate, useLocation } from "react-router-dom"; import { getMe, getLoginUrl, logout, MeResponse, getAdminMe, AdminMeResponse } from "./api"; import { GamePage } from "./pages/GamePage"; import { AdminPage } from "./pages/AdminPage"; +import { CrashPage } from "./pages/CrashPage"; const AdminRoute: React.FC<{ adminInfo: AdminMeResponse | null; children: React.ReactNode }> = ({ adminInfo, children }) => { if (!adminInfo || !adminInfo.is_admin) { @@ -51,7 +52,7 @@ const App: React.FC = () => { if (loading) { return ( -
+
Lade Bierbaron Casino...
); @@ -72,7 +73,7 @@ const App: React.FC = () => { style={{ position: "relative", width: "100%", - maxWidth: location.pathname.startsWith('/admin') ? "1200px" : "960px", // Admin-Seite breiter + maxWidth: location.pathname.startsWith('/admin') ? "1200px" : "960px", background: "rgba(10,10,18,0.95)", borderRadius: "18px", padding: "24px 28px 30px", @@ -91,15 +92,17 @@ const App: React.FC = () => { flexWrap: 'wrap' }} > -
+

🍺 Bierbaron Casino

- {adminInfo?.is_admin && ( - - 🛠 Admin Panel - - )} +
@@ -146,17 +149,20 @@ const App: React.FC = () => {
- - } /> - - - - } - /> - +
+ + } /> + } /> + + + + } + /> + +
); diff --git a/frontend/src/pages/CrashPage.tsx b/frontend/src/pages/CrashPage.tsx new file mode 100644 index 0000000..d8ea3d5 --- /dev/null +++ b/frontend/src/pages/CrashPage.tsx @@ -0,0 +1,112 @@ +// frontend/src/pages/CrashPage.tsx +import React, { useState, useEffect, useRef } from 'react'; +import { MeResponse } from '../api'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +interface CrashPageProps { + me: MeResponse | null; +} + +export const CrashPage: React.FC = ({ me }) => { + const [phase, setPhase] = useState('connecting'); + const [multiplier, setMultiplier] = useState(1.00); + const [history, setHistory] = useState<{ time: number, value: number }[]>([]); + const ws = useRef(null); + + useEffect(() => { + // Die WebSocket-URL muss auf deine Domain zeigen, aber mit wss:// (für https) + const wsUrl = `wss://${window.location.host}`; + ws.current = new WebSocket(wsUrl); + + ws.current.onopen = () => { + console.log("WebSocket connected"); + setPhase('connected'); + }; + + ws.current.onmessage = (event) => { + const data = JSON.parse(event.data); + + switch (data.type) { + case 'gameState': + setPhase(data.phase); + setMultiplier(data.multiplier); + break; + case 'newRound': + setPhase(data.phase); + setHistory([]); // Graphen zurücksetzen + setMultiplier(1.00); + break; + case 'roundStart': + setPhase(data.phase); + break; + case 'multiplierUpdate': + setMultiplier(data.multiplier); + setHistory(prev => [...prev, { time: prev.length, value: data.multiplier }]); + break; + case 'crash': + setPhase('crashed'); + setMultiplier(data.multiplier); + break; + } + }; + + ws.current.onclose = () => { + console.log("WebSocket disconnected"); + setPhase('disconnected'); + }; + + return () => { + ws.current?.close(); + }; + }, []); + + const getStatusMessage = () => { + switch (phase) { + case 'connecting': return 'Verbinde mit dem Server...'; + case 'connected': + case 'waiting': return 'Warte auf die nächste Runde...'; + case 'betting': return 'Einsätze platzieren! Runde startet bald...'; + case 'running': return 'Runde läuft!'; + case 'crashed': return `CRASHED @ ${multiplier.toFixed(2)}x`; + case 'disconnected': return 'Verbindung verloren. Bitte Seite neu laden.'; + default: return ''; + } + }; + + return ( +
+

🚀 Bier-Crash

+
+ {/* Linke Spalte: Steuerung */} +
+

Dein Einsatz

+ + + +
+ + {/* Rechte Spalte: Graph und Multiplikator */} +
+
+

+ {multiplier.toFixed(2)}x +

+

{getStatusMessage()}

+
+ + + + + null} /> + + + +
+
+
+ ); +}; \ No newline at end of file