+ {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 }) {
+ 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 api(`/tools/schocken/${game.id}/roll`, {
+ body: { keep_dice: keepDice, flip_sixes: flipSixes, go_dark: goDark }
+ });
+ onRefresh();
+ };
+
+ const handleReady = async () => {
+ await api(`/tools/schocken/${game.id}/ready`, { body: {} });
+ onRefresh();
+ };
+
+ const handleEvaluate = async () => {
+ const result = await api(`/tools/schocken/${game.id}/evaluate`, { body: {} });
+ if (result.round_result?.eventMsg) toast(result.round_result.eventMsg);
+ onRefresh();
+ };
+
+ const handleStart = async () => {
+ await api(`/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}
+
+
+ {/* 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 myId = getMyId();
+
+ 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
+ ?
+ : Lade…
+ }
+
+ {showNew && setShowNew(false)} toast={toast} onCreate={id=>{setShowNew(false);setActiveId(id);loadGames();}}/>}
+
+ );
+}