import { useState, useEffect, useCallback } from 'react'; import { api, S } from '../lib.js'; // Testmodus: API-Call als anderer User (nur Admin, nur Schocken) async function apiAs(userId, path, opts = {}) { const token = localStorage.getItem('sk_token'); const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; if (userId) headers['X-Schocken-Test-As'] = String(userId); const res = await fetch(`/api${path}`, { method: opts.method || (opts.body ? 'POST' : 'GET'), headers, body: opts.body ? JSON.stringify(opts.body) : undefined, }); if (!res.ok) { const e = await res.json().catch(()=>({error:'Fehler'})); throw new Error(e.error||'Fehler'); } return res.json(); } const MY_COLOR = '#4ecdc4'; function getMyId() { try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).id; } catch { return null; } } function getMyRole() { try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; } } // ── Würfel-Visualisierung ───────────────────────────────────────────────────── const DOTS = { 1: [[50,50]], 2: [[25,25],[75,75]], 3: [[25,25],[50,50],[75,75]], 4: [[25,25],[75,25],[25,75],[75,75]], 5: [[25,25],[75,25],[50,50],[25,75],[75,75]], 6: [[25,25],[75,25],[25,50],[75,50],[25,75],[75,75]], }; function Die({ value, kept, selected, onClick, dark, size=56 }) { const dots = value ? DOTS[value] : []; const bg = dark ? 'rgba(40,40,60,0.9)' : kept ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.08)'; const border = selected ? '2px solid #4ecdc4' : kept ? '2px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.15)'; return (
{dark &&
🎲
} {!dark && dots.map(([x,y],i) => (
40?10:7, height:size>40?10:7, borderRadius:'50%', background:'#fff', }}/> ))} {!value && !dark &&
?
}
); } // ── Spieler-Zeile ───────────────────────────────────────────────────────────── function PlayerRow({ player, chips, has16th, isMe, isCurrent, result, drinkLosses, game }) { const isLoserH1 = game.has_16th === player.id; return (
{isCurrent && } {player.username} {isMe && (du)} {isLoserH1 && 🪶}
{drinkLosses > 0 && ( 🍺×{drinkLosses} )}
0?'#ff6b9d':MY_COLOR,fontFamily:'Space Mono,monospace',fontSize:18,fontWeight:700,lineHeight:1}}>{chips}
Scheiben
{result && (
{result.label}
)}
); } // ── Würfelbereich ───────────────────────────────────────────────────────────── function DiceArea({ game, myId, onRoll, onReady, onEvaluate }) { const [selectedKeep, setSelectedKeep] = useState([]); const [goDark, setGoDark] = useState(false); const [flipSixes, setFlipSixes] = useState(false); const [rolling, setRolling] = useState(false); const round = game.current_round; const players = game.players; const activePlayers = getActivePlayers(game); const myState = round?.playerStates?.[myId]; const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length]; const isMyTurn = currentPlayer?.id === myId; const allReady = activePlayers.every(p => round?.ready?.[p.id]); const allDone = round?.phase === 'reveal'; const isFirstRound = !!game.first_round; const myDice = myState?.dice || [null, null, null]; const myRolls = myState?.roll_count || 0; const maxRolls = round?.max_rolls || 3; const canFlipSixes = myDice.filter(d => d===6).length >= 2 && myRolls < maxRolls - 1; // Becher umdrehen Phase — warte bis alle ready if (!round || !allReady) { const iMReady = round?.ready?.[myId]; const readyCount = Object.keys(round?.ready || {}).length; const activeCount = activePlayers.length; return (
{readyCount}/{activeCount} haben den Becher umgedreht
{!iMReady && ( )} {iMReady && (
✓ Becher umgedreht — warte auf andere…
)} {isFirstRound && iMReady && readyCount === activeCount && (
Alle bereit — Becher werden gleichzeitig gehoben!
)}
); } // Auswertungsphase if (allDone) { const isFirstPlayer = activePlayers[0]?.id === myId || players[0]?.id === myId; return (
Alle haben gewürfelt — Ergebnis auswerten!
{isFirstPlayer && ( )}
); } // Mein Würfeln return (
{/* Status */}
{isMyTurn ? `▶ Dein Wurf (${myRolls}/${maxRolls||'?'})` : `⏳ ${currentPlayer?.username} würfelt…`}
{/* Würfel */}
{myDice.map((d, i) => ( 0 && !myState?.done ? () => { setSelectedKeep(prev => prev.includes(i) ? prev.filter(x=>x!==i) : [...prev,i] ); } : undefined} size={64} /> ))}
{/* Optionen */} {isMyTurn && !myState?.done && (
{myRolls > 0 && (
{selectedKeep.length > 0 && 📌 {selectedKeep.length} Würfel stehen lassen}
)} {canFlipSixes && ( )} {myRolls > 0 && myRolls < (maxRolls||3) && ( )}
)} {myState?.done && !allDone && (
{myState.dark ? '🎲 Du bist dunkel — warte auf andere…' : `✓ Du stehst im ${myRolls}. — warte auf andere…`}
)} {/* Andere Spieler Status */}
{activePlayers.filter(p => p.id !== myId).map(p => { const ps = round.playerStates?.[p.id]; return (
{ps?.done ? '✓' : '⏳'} {p.username} {ps?.dark && '🌑'}
); })}
); } // ── Schockstock ─────────────────────────────────────────────────────────────── function Schockstock({ total=15, stock, players, chips }) { const used = total - stock; return (
SCHOCKSTOCK {stock} verbleibend
{Array.from({length:16}).map((_,i) => { const is16th = i === 15; const isUsed = !is16th && i >= stock; return (
); })}
); } // ── Neue Runde Button ───────────────────────────────────────────────────────── function NewRoundButton({ game, myId, onReady }) { // Der Beginner der neuen Runde startet mit Becher umdrehen const isStarter = game.beginner_id === myId; return (
Neue Runde — {isStarter ? 'du fängst an!' : `${game.players.find(p=>p.id===game.beginner_id)?.username} fängt an`}
); } // ── Spielende ───────────────────────────────────────────────────────────────── function GameOver({ game }) { const dl = game.drink_losses; const loser = game.players.find(p => dl[p.id] > 0); const isDoppelfeige = game.loser_h1 === game.loser_h2; return (
{isDoppelfeige ? '🪶🪶' : '🍺'}
{isDoppelfeige ? (
DOPPELFEIGE!
) : (
RUNDE VORBEI
)} {loser && (
{loser.username} zahlt die nächste Runde! 🍺
)}
GETRÄNKE-STATISTIK
{game.players.map(p => (
{p.username} 0?'#f59e0b':MY_COLOR}}>🍺 {dl[p.id]||0}
))}
); } // ── Aktive Spieler berechnen (Frontend) ─────────────────────────────────────── function getActivePlayers(game) { const { players, player_chips, stock, status, loser_h1, loser_h2 } = game; if (status === 'endkampf') { return players.filter(p => p.id === loser_h1 || p.id === loser_h2); } if (stock > 0) return players; return players.filter(p => player_chips[p.id] > 0); } // ── Neues Spiel Modal ───────────────────────────────────────────────────────── function NewGameModal({ onClose, onCreate, toast }) { const [users, setUsers] = useState([]); const [picked, setPicked] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { api('/tools/schocken/users').then(setUsers).catch(()=>{}); }, []); const toggle = id => setPicked(p => p.includes(id) ? p.filter(x=>x!==id) : [...p,id]); const create = async () => { if (!picked.length) return; setLoading(true); try { const r = await api('/tools/schocken', { body: { player_ids: picked } }); toast('Spiel erstellt! 🎲'); onCreate(r.id); } catch(e) { toast(e.message||'Fehler','error'); } finally { setLoading(false); } }; return (
NEUES SPIEL
MITSPIELER WÄHLEN
{users.map(u => (
toggle(u.id)} style={{ display:'flex',alignItems:'center',gap:10,padding:'8px 10px', cursor:'pointer',borderRadius:8,marginBottom:4, background: picked.includes(u.id) ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.03)', border: picked.includes(u.id) ? '1px solid rgba(78,205,196,0.3)' : '1px solid rgba(255,255,255,0.06)', }}> {picked.includes(u.id)?'☑':'☐'} {u.username}
))}
); } // ── Spielliste ──────────────────────────────────────────────────────────────── function GameList({ games, myId, onSelect, onNew }) { const active = games.filter(g => g.status !== 'finished'); const finished = games.filter(g => g.status === 'finished'); const statusLabel = s => ({ lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet' }[s]||s); const GameRow = ({ g }) => { const others = g.players.filter(p=>p.id!==myId).map(p=>p.username).join(', '); return ( ); }; return (
{active.length > 0 && <>
LAUFEND ({active.length})
{active.map(g=>)} } {finished.length > 0 && <>
BEENDET
{finished.map(g=>)} } {games.length===0 && (
Noch keine Spiele — starte eines!
)}
); } // ── Spielansicht ────────────────────────────────────────────────────────────── function GameView({ game, myId, onRefresh, toast, isAdmin=false, onSeatChange }) { const players = game.players; const chips = game.player_chips; const dl = game.drink_losses; const round = game.current_round; const activePlayers = getActivePlayers(game); const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length]; const handleRoll = async (keepDice, flipSixes, goDark) => { await apiAs(myId, `/tools/schocken/${game.id}/roll`, { body: { keep_dice: keepDice, flip_sixes: flipSixes, go_dark: goDark } }); onRefresh(); }; const handleReady = async () => { await apiAs(myId, `/tools/schocken/${game.id}/ready`, { body: {} }); onRefresh(); }; const handleEvaluate = async () => { const result = await apiAs(myId, `/tools/schocken/${game.id}/evaluate`, { body: {} }); if (result.round_result?.eventMsg) toast(result.round_result.eventMsg); onRefresh(); }; const handleStart = async () => { await apiAs(myId, `/tools/schocken/${game.id}/start`, { body: {} }); onRefresh(); }; const statusLabel = { lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet' }; return (
{/* Phase-Header */}
{statusLabel[game.status]} Runde {game.round_number}
{/* Testmodus-Switcher (nur Admin) */} {isAdmin && (
🧪 TESTMODUS — SPIELER WECHSELN
{players.map(p => ( ))}
)} {/* Schockstock */} {/* Spieler */}
{players.map(p => ( ))}
{/* Lobby */} {game.status==='lobby' && (
{players[0]?.id===myId ? ( ) : (
Warte auf {players[0]?.username} um das Spiel zu starten…
)}
)} {/* Spielfeld */} {game.status!=='lobby' && game.status!=='finished' && (
)} {/* Spielende */} {game.status==='finished' && }
); } // ── Hauptkomponente ─────────────────────────────────────────────────────────── export default function Schocken({ toast }) { const [games, setGames] = useState([]); const [activeId, setActiveId] = useState(null); const [game, setGame] = useState(null); const [showNew, setShowNew] = useState(false); const [loading, setLoading] = useState(true); const [testSeat, setTestSeat] = useState(null); // null = eigener Account, sonst userId const myRealId = getMyId(); const isAdmin = getMyRole() === 'admin'; // Im Testmodus: als anderen Spieler agieren const myId = (isAdmin && testSeat) ? testSeat : myRealId; const loadGames = useCallback(async () => { try { setGames(await api('/tools/schocken')); } catch { toast?.('Fehler beim Laden','error'); } finally { setLoading(false); } }, []); const loadGame = useCallback(async () => { if (!activeId) { setGame(null); return; } try { setGame(await api(`/tools/schocken/${activeId}`)); } catch {} }, [activeId]); useEffect(() => { loadGames(); }, [loadGames]); useEffect(() => { loadGame(); }, [loadGame]); // Polling wenn Spiel aktiv useEffect(() => { if (!activeId) return; const iv = setInterval(loadGame, 5000); return () => clearInterval(iv); }, [activeId, loadGame]); if (loading) return
Lade…
; return (
{activeId && ( )}

🎲 SCHOCKEN

{!activeId ? setShowNew(true)}/> : game && myId ? {setTestSeat(id);}} /> :
Lade…
} {showNew && setShowNew(false)} toast={toast} onCreate={id=>{setShowNew(false);setActiveId(id);loadGames();}}/>}
); }