| <!DOCTYPE html> |
| <html lang="de"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>31 CARD | Casino Online</title> |
|
|
| <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> |
| <script src="https://cdn.tailwindcss.com"></script> |
|
|
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;900&display=swap'); |
| body { font-family: 'Inter', sans-serif; margin: 0; padding: 0; } |
| |
| .main-wrapper { |
| display: flex; flex-direction: column; justify-content: center; |
| align-items: center; min-height: 100vh; width: 100%; |
| } |
| |
| .card { |
| transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| cursor: pointer; aspect-ratio: 2/3; min-width: 80px; max-width: 110px; user-select: none; |
| } |
| .card.selected { |
| border: 4px solid #d4af37; box-shadow: 0 0 25px rgba(212, 175, 55, 0.5); transform: translateY(-20px) scale(1.05); |
| } |
| .poker-bg { background: radial-gradient(circle at center, #1a1c2c 0%, #0a0a0a 100%); } |
| #game-area.hidden { display: none; } |
| |
| #cookie-banner { |
| position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); |
| width: 90%; max-width: 600px; background: rgba(17, 24, 39, 0.95); |
| backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); |
| padding: 20px; border-radius: 24px; z-index: 1000; display: none; |
| } |
| </style> |
| </head> |
| <body class="poker-bg text-white overflow-x-hidden"> |
|
|
| <div class="main-wrapper p-4"> |
| <div class="w-full max-w-4xl bg-gray-900/80 backdrop-blur-2xl rounded-[2.5rem] shadow-2xl p-6 sm:p-10 border border-white/10"> |
|
|
| <div id="login-area" class="text-center py-10"> |
| <h1 class="text-7xl font-black text-yellow-500 mb-4 italic tracking-tighter drop-shadow-lg text-center uppercase">31 Card</h1> |
| <p class="mb-10 text-gray-400 uppercase tracking-[0.3em] text-[10px] font-bold">Spiel 31 Karten Ultimative PRO</p> |
|
|
| <div class="max-w-xs mx-auto space-y-4"> |
| <input type="email" id="email" placeholder="E-Mail Adresse" |
| class="bg-gray-800/50 border border-gray-700 text-white px-6 py-4 rounded-2xl w-full outline-none focus:border-yellow-500 text-center text-lg transition-all"> |
| <button onclick="doLogin()" class="bg-red-600 hover:bg-red-500 w-full py-4 rounded-2xl font-black uppercase tracking-widest transition-all shadow-xl active:scale-95"> |
| Spiel beitreten |
| </button> |
| <p id="connection-status" class="text-[10px] text-gray-500 uppercase mt-6 tracking-widest font-bold font-sans">Checking Server...</p> |
| </div> |
| </div> |
|
|
| <div id="game-area" class="hidden"> |
| <header class="flex justify-between items-start border-b border-white/5 pb-6 mb-8"> |
| <div class="flex gap-6 sm:gap-10"> |
| <div> |
| <span class="block text-[10px] text-gray-500 uppercase font-black mb-1 tracking-widest">Status</span> |
| <h2 id="status-text" class="text-sm font-black uppercase text-white italic leading-tight">Warten...</h2> |
| </div> |
| <div> |
| <span class="block text-[10px] text-yellow-500 uppercase font-black mb-1 tracking-widest">Einsatz</span> |
| <h2 id="display-bet" class="text-sm font-black text-white italic leading-tight">0 $</h2> |
| </div> |
| <div> |
| <span class="block text-[10px] text-green-500 uppercase font-black mb-1 tracking-widest">Pott</span> |
| <h2 id="display-pott" class="text-sm font-black text-white italic leading-tight">0 $</h2> |
| </div> |
| </div> |
| |
| <div class="flex items-center gap-6"> |
| <div class="text-right"> |
| <p class="text-[10px] text-gray-500 uppercase font-black tracking-widest">Bankroll</p> |
| <p id="user-coins" class="text-2xl font-black text-green-400">0 $</p> |
| </div> |
| <button onclick="doLogout()" class="bg-red-500/10 hover:bg-red-500/20 text-red-500 p-3 rounded-2xl border border-red-500/20 active:scale-90 transition-all"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> |
| </svg> |
| </button> |
| </div> |
| </header> |
|
|
| <div id="admin-panel" class="hidden bg-yellow-500/10 p-5 rounded-3xl border border-yellow-500/20 mb-8 flex flex-wrap items-center justify-between gap-4"> |
| <div class="flex flex-col"> |
| <span class="text-[10px] text-yellow-500 font-black uppercase tracking-widest">Admin Control</span> |
| <div class="flex items-center gap-2 mt-2"> |
| <input type="number" id="bet-input" value="50" class="bg-gray-800 border border-gray-700 text-yellow-500 text-xs font-black px-2 py-1 rounded w-16 outline-none"> |
| <button onclick="changeBet()" class="text-[10px] bg-gray-700 text-white px-2 py-1 rounded font-bold uppercase hover:bg-gray-600 transition-all">Set Bet</button> |
| </div> |
| </div> |
| <div class="flex gap-2"> |
| <button onclick="startRound()" class="bg-yellow-500 hover:bg-yellow-400 text-black px-4 py-2 rounded-xl font-black uppercase text-xs transition-all">Start</button> |
| <button onclick="reshuffle()" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-xl font-black uppercase text-xs transition-all">Mischen</button> |
| <button onclick="endGame()" class="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-xl font-black uppercase text-xs transition-all">Ende</button> |
| </div> |
| </div> |
|
|
| <div id="middle-cards" class="flex justify-center gap-4 sm:gap-8 mb-16 min-h-[150px] items-center"></div> |
|
|
| <div class="bg-white/5 p-8 rounded-[3rem] border border-white/10 relative"> |
| <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-gray-900 px-6 py-1 rounded-full border border-white/10 text-[10px] text-gray-400 font-black uppercase tracking-widest"> |
| SCORE: <span id="score" class="text-yellow-500 ml-1">0</span> |
| </div> |
| <div id="player-hand" class="flex justify-center gap-4 sm:gap-8 mb-10 min-h-[150px] items-center"></div> |
| |
| <div class="flex flex-wrap justify-center gap-4"> |
| <button onclick="action('swap_all')" class="bg-gray-800 hover:bg-gray-700 px-6 py-4 rounded-2xl font-black text-[10px] uppercase border border-white/5 tracking-widest transition-all">Tauschen (Alle)</button> |
| <button onclick="action('pass')" class="bg-blue-600 hover:bg-blue-500 px-6 py-4 rounded-2xl font-black text-[10px] uppercase border border-white/5 tracking-widest transition-all">Schieben (Weiter)</button> |
| <button onclick="action('knock')" class="bg-red-600 hover:bg-red-500 px-10 py-4 rounded-2xl font-black text-[10px] uppercase shadow-2xl border border-red-400/20 tracking-widest transition-all">Klopfen</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="cookie-banner"> |
| <div class="flex flex-col sm:flex-row items-center justify-between gap-6"> |
| <div class="text-[12px] text-gray-300"> |
| <p class="font-black text-white mb-1 uppercase tracking-wider">🍪 Data Privacy</p> |
| Diese App nutzt Cookies für die Sitzung. |
| </div> |
| <button onclick="acceptCookies()" class="bg-white text-black px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-widest">OK</button> |
| </div> |
| </div> |
|
|
| <script> |
| const socket = io({ transports: ['websocket'] }); |
| let myEmail = "", selHandIdx = null; |
| |
| window.onload = function() { |
| if(!localStorage.getItem('cookiesAccepted')) { |
| document.getElementById('cookie-banner').style.display = 'block'; |
| } |
| }; |
| |
| function acceptCookies() { |
| localStorage.setItem('cookiesAccepted', 'true'); |
| document.getElementById('cookie-banner').style.display = 'none'; |
| } |
| |
| async function doLogin() { |
| myEmail = document.getElementById('email').value; |
| if(!myEmail.includes('@')) return alert("Bitte E-Mail eingeben!"); |
| |
| const res = await fetch(`/login`, { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({email: myEmail}) |
| }); |
| const data = await res.json(); |
| document.getElementById('user-coins').innerText = data.stats.coins + " $"; |
| if(data.stats.is_admin) document.getElementById('admin-panel').classList.remove('hidden'); |
| document.getElementById('login-area').classList.add('hidden'); |
| document.getElementById('game-area').classList.remove('hidden'); |
| socket.emit('join_game', {email: myEmail}); |
| } |
| |
| function doLogout() { location.reload(); } |
| function startRound() { socket.emit('start_round', {email: myEmail}); } |
| function reshuffle() { socket.emit('admin/reshuffle', {email: myEmail}); } |
| function endGame() { socket.emit('admin/end_game', {email: myEmail}); } |
| function changeBet() { socket.emit('admin/update_bet', { email: myEmail, bet: document.getElementById('bet-input').value }); } |
| function action(t) { socket.emit('player_action', {email: myEmail, type: t}); } |
| |
| socket.on('update_table', (data) => { |
| const me = data.players.find(p => p.email === myEmail); |
| const isMyTurn = data.players[data.turn_idx]?.email === myEmail; |
| |
| document.getElementById('status-text').innerText = isMyTurn ? "DEIN ZUG!" : "WARTE..."; |
| document.getElementById('status-text').className = isMyTurn ? "text-sm font-black text-green-400 animate-pulse uppercase tracking-widest" : "text-sm font-black text-gray-500 uppercase tracking-widest"; |
| |
| document.getElementById('display-bet').innerText = data.current_bet + " $"; |
| document.getElementById('display-pott').innerText = (data.pott || 0) + " $"; |
| |
| const getSymbol = (s) => ({ 'Herz': '♥', 'Karo': '♦', 'Pik': '♠', 'Kreuz': '♣' }[s]); |
| const getColor = (s) => (s === 'Herz' || s === 'Karo') ? 'text-red-600' : 'text-gray-900'; |
| const cardHTML = (c, onClick, sel = false) => ` |
| <div onclick="${onClick}" class="card bg-white rounded-2xl flex flex-col items-center justify-between p-4 shadow-2xl text-black ${sel ? 'selected' : ''}"> |
| <div class="self-start font-black text-xl leading-none ${getColor(c.suit)}">${c.rank}</div> |
| <div class="text-5xl ${getColor(c.suit)}">${getSymbol(c.suit)}</div> |
| <div class="self-end font-black text-xl leading-none rotate-180 ${getColor(c.suit)}">${c.rank}</div> |
| </div>`; |
| |
| document.getElementById('middle-cards').innerHTML = data.middle.map((c, i) => cardHTML(c, `swap(${i})`)).join(''); |
| if(me) { |
| document.getElementById('user-coins').innerText = me.coins + " $"; |
| document.getElementById('score').innerText = me.score; |
| document.getElementById('player-hand').innerHTML = me.hand.length > 0 ? |
| me.hand.map((c, i) => cardHTML(c, `selectHand(${i})`, selHandIdx === i)).join('') : |
| `<p class="text-gray-400 italic text-[10px] mt-10 uppercase font-bold">Warte auf Start...</p>`; |
| } |
| }); |
| |
| function selectHand(i) { selHandIdx = (selHandIdx === i) ? null : i; socket.emit('join_game', {email: myEmail}); } |
| function swap(mIdx) { if(selHandIdx !== null) { socket.emit('player_action', {email: myEmail, type: 'swap_one', h_idx: selHandIdx, m_idx: mIdx}); selHandIdx = null; } } |
| |
| socket.on('game_over', (d) => { |
| alert(`RUNDE BEENDET!\n\nGewinner: ${d.winner}\nPunkte: ${d.score}\nPott: ${d.pott} $`); |
| }); |
| </script> |
| </body> |
| </html> |